From aaff68c0d55dc32f8972b9bde3239bb0923de140 Mon Sep 17 00:00:00 2001 From: UnicronNL Date: Sun, 1 Jul 2018 22:48:06 +0200 Subject: add vyos 1.x --- .gitignore | 117 ++++ LICENSE.GPL | 339 +++++++++++ LICENSE.LGPL | 502 +++++++++++++++ Makefile | 53 ++ README.md | 56 ++ data/interface-types.json | 17 + debian/changelog | 40 ++ debian/compat | 1 + debian/control | 39 ++ debian/copyright | 35 ++ debian/lintian-overrides | 6 + debian/rules | 61 ++ interface-definitions/bcast-relay.xml | 68 +++ interface-definitions/beep-on-boot.xml | 21 + interface-definitions/cron.xml | 75 +++ interface-definitions/dns-forwarding.xml | 135 +++++ interface-definitions/host-name.xml | 26 + interface-definitions/mdns-repeater.xml | 32 + interface-definitions/ntp.xml | 89 +++ interface-definitions/snmp.xml | 606 +++++++++++++++++++ interface-definitions/ssh.xml | 157 +++++ op-mode-definitions/bandwidth-monitor.xml | 23 + op-mode-definitions/dns-forwarding.xml | 72 +++ op-mode-definitions/poweroff.xml | 40 ++ op-mode-definitions/reboot.xml | 40 ++ op-mode-definitions/show-arp.xml | 25 + op-mode-definitions/show-bridge.xml | 37 ++ op-mode-definitions/show-configuration.xml | 39 ++ op-mode-definitions/show-date.xml | 30 + op-mode-definitions/show-disk.xml | 24 + op-mode-definitions/show-hardware.xml | 95 +++ op-mode-definitions/show-raid.xml | 18 + op-mode-definitions/show-users.xml | 30 + op-mode-definitions/snmp.xml | 111 ++++ op-mode-definitions/traffic-dump.xml | 45 ++ op-mode-definitions/version.xml | 27 + python/setup.py | 21 + python/vyos/__init__.py | 1 + python/vyos/base.py | 18 + python/vyos/config.py | 416 +++++++++++++ python/vyos/configtree.py | 261 ++++++++ python/vyos/defaults.py | 19 + python/vyos/interfaces.py | 45 ++ python/vyos/limericks.py | 64 ++ python/vyos/util.py | 65 ++ python/vyos/version.py | 68 +++ schema/interface_definition.rnc | 154 +++++ schema/interface_definition.rng | 275 +++++++++ schema/op-mode-definition.rnc | 107 ++++ schema/op-mode-definition.rng | 165 +++++ scripts/build-command-op-templates | 225 +++++++ scripts/build-command-templates | 295 +++++++++ sonar-project.properties | 21 + sphinx/Makefile | 177 ++++++ sphinx/source/conf.py | 261 ++++++++ sphinx/source/index.rst | 22 + src/completion/list_disks.sh | 5 + src/completion/list_dumpable_interfaces.py | 14 + src/completion/list_interfaces.py | 31 + src/completion/list_raidset.sh | 3 + src/conf_mode/bcast_relay.py | 122 ++++ src/conf_mode/beep_if_fully_booted.py | 42 ++ src/conf_mode/dns_forwarding.py | 234 +++++++ src/conf_mode/host_name.py | 117 ++++ src/conf_mode/lldp.py | 217 +++++++ src/conf_mode/mdns_repeater.py | 93 +++ src/conf_mode/ntp.py | 173 ++++++ src/conf_mode/snmp.py | 804 +++++++++++++++++++++++++ src/conf_mode/ssh.py | 255 ++++++++ src/conf_mode/task_scheduler.py | 148 +++++ src/helpers/commands-pipe.py | 29 + src/helpers/validate-value.py | 43 ++ src/migration-scripts/config-management/0-to-1 | 31 + src/migration-scripts/system/7-to-8 | 45 ++ src/op_mode/cpu_summary.py | 24 + src/op_mode/dns_forwarding_reset.py | 49 ++ src/op_mode/dns_forwarding_restart.sh | 8 + src/op_mode/dns_forwarding_statistics.py | 33 + src/op_mode/maya_date.py | 209 +++++++ src/op_mode/show-configuration-files.sh | 10 + src/op_mode/show-disk-format.sh | 8 + src/op_mode/show-raid.sh | 17 + src/op_mode/snmp.py | 77 +++ src/op_mode/snmp_ifmib.py | 128 ++++ src/op_mode/snmp_v3.py | 180 ++++++ src/op_mode/snmp_v3_showcerts.sh | 8 + src/op_mode/version.py | 123 ++++ src/tests/helper.py | 27 + src/tests/test_host_name.py | 130 ++++ src/tests/test_task_scheduler.py | 130 ++++ src/utils/initial-setup | 40 ++ src/utils/vyos-config-to-commands | 29 + src/validators/interface-address | 3 + src/validators/ip-address | 3 + src/validators/ip-host | 3 + src/validators/ip-prefix | 3 + src/validators/ipv4-address | 3 + src/validators/ipv4-host | 3 + src/validators/ipv4-prefix | 3 + src/validators/ipv6-address | 3 + src/validators/ipv6-host | 3 + src/validators/ipv6-prefix | 3 + src/validators/numeric | 62 ++ test-requirements.txt | 5 + tests/data/interface-definitions/test-op.xml | 21 + tests/data/interface-definitions/test.xml | 24 + 106 files changed, 9589 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.GPL create mode 100644 LICENSE.LGPL create mode 100644 Makefile create mode 100644 README.md create mode 100644 data/interface-types.json create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/lintian-overrides create mode 100755 debian/rules create mode 100644 interface-definitions/bcast-relay.xml create mode 100644 interface-definitions/beep-on-boot.xml create mode 100644 interface-definitions/cron.xml create mode 100644 interface-definitions/dns-forwarding.xml create mode 100644 interface-definitions/host-name.xml create mode 100644 interface-definitions/mdns-repeater.xml create mode 100644 interface-definitions/ntp.xml create mode 100644 interface-definitions/snmp.xml create mode 100644 interface-definitions/ssh.xml create mode 100644 op-mode-definitions/bandwidth-monitor.xml create mode 100644 op-mode-definitions/dns-forwarding.xml create mode 100644 op-mode-definitions/poweroff.xml create mode 100644 op-mode-definitions/reboot.xml create mode 100644 op-mode-definitions/show-arp.xml create mode 100644 op-mode-definitions/show-bridge.xml create mode 100644 op-mode-definitions/show-configuration.xml create mode 100644 op-mode-definitions/show-date.xml create mode 100644 op-mode-definitions/show-disk.xml create mode 100644 op-mode-definitions/show-hardware.xml create mode 100644 op-mode-definitions/show-raid.xml create mode 100644 op-mode-definitions/show-users.xml create mode 100644 op-mode-definitions/snmp.xml create mode 100644 op-mode-definitions/traffic-dump.xml create mode 100644 op-mode-definitions/version.xml create mode 100644 python/setup.py create mode 100644 python/vyos/__init__.py create mode 100644 python/vyos/base.py create mode 100644 python/vyos/config.py create mode 100644 python/vyos/configtree.py create mode 100644 python/vyos/defaults.py create mode 100644 python/vyos/interfaces.py create mode 100644 python/vyos/limericks.py create mode 100644 python/vyos/util.py create mode 100644 python/vyos/version.py create mode 100644 schema/interface_definition.rnc create mode 100644 schema/interface_definition.rng create mode 100644 schema/op-mode-definition.rnc create mode 100644 schema/op-mode-definition.rng create mode 100755 scripts/build-command-op-templates create mode 100755 scripts/build-command-templates create mode 100644 sonar-project.properties create mode 100644 sphinx/Makefile create mode 100644 sphinx/source/conf.py create mode 100644 sphinx/source/index.rst create mode 100755 src/completion/list_disks.sh create mode 100755 src/completion/list_dumpable_interfaces.py create mode 100755 src/completion/list_interfaces.py create mode 100755 src/completion/list_raidset.sh create mode 100755 src/conf_mode/bcast_relay.py create mode 100755 src/conf_mode/beep_if_fully_booted.py create mode 100755 src/conf_mode/dns_forwarding.py create mode 100755 src/conf_mode/host_name.py create mode 100644 src/conf_mode/lldp.py create mode 100755 src/conf_mode/mdns_repeater.py create mode 100755 src/conf_mode/ntp.py create mode 100755 src/conf_mode/snmp.py create mode 100755 src/conf_mode/ssh.py create mode 100755 src/conf_mode/task_scheduler.py create mode 100755 src/helpers/commands-pipe.py create mode 100755 src/helpers/validate-value.py create mode 100755 src/migration-scripts/config-management/0-to-1 create mode 100755 src/migration-scripts/system/7-to-8 create mode 100755 src/op_mode/cpu_summary.py create mode 100755 src/op_mode/dns_forwarding_reset.py create mode 100755 src/op_mode/dns_forwarding_restart.sh create mode 100755 src/op_mode/dns_forwarding_statistics.py create mode 100755 src/op_mode/maya_date.py create mode 100755 src/op_mode/show-configuration-files.sh create mode 100755 src/op_mode/show-disk-format.sh create mode 100755 src/op_mode/show-raid.sh create mode 100755 src/op_mode/snmp.py create mode 100755 src/op_mode/snmp_ifmib.py create mode 100755 src/op_mode/snmp_v3.py create mode 100755 src/op_mode/snmp_v3_showcerts.sh create mode 100755 src/op_mode/version.py create mode 100644 src/tests/helper.py create mode 100644 src/tests/test_host_name.py create mode 100644 src/tests/test_task_scheduler.py create mode 100644 src/utils/initial-setup create mode 100755 src/utils/vyos-config-to-commands create mode 100755 src/validators/interface-address create mode 100755 src/validators/ip-address create mode 100755 src/validators/ip-host create mode 100755 src/validators/ip-prefix create mode 100755 src/validators/ipv4-address create mode 100755 src/validators/ipv4-host create mode 100755 src/validators/ipv4-prefix create mode 100755 src/validators/ipv6-address create mode 100755 src/validators/ipv6-host create mode 100755 src/validators/ipv6-prefix create mode 100755 src/validators/numeric create mode 100644 test-requirements.txt create mode 100644 tests/data/interface-definitions/test-op.xml create mode 100644 tests/data/interface-definitions/test.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5b100a --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# 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/* + +# 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/ + +# 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-1x +debian/vyos-1x.* + +# Sonar Cloud +.scannerwork diff --git a/LICENSE.GPL b/LICENSE.GPL new file mode 100644 index 0000000..23cb790 --- /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., + 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 0000000..4362b49 --- /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. + + + Copyright (C) + + 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. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50710af --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +TMPL_DIR := templates-cfg +OP_TMPL_DIR := templates-op + +.PHONY: interface_definitions +.ONESHELL: +interface_definitions: + mkdir -p $(TMPL_DIR) + + find $(CURDIR)/interface-definitions/ -type f | 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)/system/node.def + rm -f $(TMPL_DIR)/system/options/node.def + rm -f $(TMPL_DIR)/service/node.def + rm -f $(TMPL_DIR)/service/dns/node.def + rm -f $(TMPL_DIR)/protocols/node.def + +.PHONY: op_mode_definitions +.ONESHELL: +op_mode_definitions: + mkdir -p $(OP_TMPL_DIR) + + find $(CURDIR)/op-mode-definitions/ -type f | 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)/show/node.def + rm -f $(OP_TMPL_DIR)/show/dns/node.def + rm -f $(OP_TMPL_DIR)/reset/node.def + rm -f $(OP_TMPL_DIR)/restart/node.def + rm -f $(OP_TMPL_DIR)/monitor/node.def + +.PHONY: all +all: clean interface_definitions op_mode_definitions + +.PHONY: clean +clean: + rm -rf $(TMPL_DIR)/* + rm -rf $(OP_TMPL_DIR)/* + +.PHONY: test +test: + 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 --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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..665512c --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# vyos-1x: VyOS 1.2.0+ configuration scripts and data + +[![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) + +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. + +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. + +## 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: + +``` +make test +``` diff --git a/data/interface-types.json b/data/interface-types.json new file mode 100644 index 0000000..c452122 --- /dev/null +++ b/data/interface-types.json @@ -0,0 +1,17 @@ +{ + "loopback": "lo", + "dummy": "dum", + "ethernet": "eth", + "bonding": "bond", + "bridge": "br", + "pseudo-ethernet": "peth", + "openvpn": "vtun", + "tunnel": "tun", + "vti": "vti", + "l2tpv3": "l2tpeth", + "vxlan": "vxlan", + "wireless": "wireless", + "wirelessmodem": "wlm", + "input": "ifb", + "pppoe": "pppoe" +} diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..0f41838 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,40 @@ +vyos-1x (1.0.5) unstable; urgency=medium + + * T606: Error in DNS Forwarder listen-on + * T608: Cannot configure broadcast-relay service + + -- Christian Poessinger Thu, 19 Apr 2018 21:16:28 +0200 + +vyos-1x (1.0.4) unstable; urgency=medium + + * T560: dns-forwarding: replace dnsmasq with pdns-recursor + * T588: Rewrite 'service dns forwarding' in new XML style format + + -- Christian Poessinger Sun, 15 Apr 2018 16:13:32 +0200 + +vyos-1x (1.0.3) unstable; urgency=medium + + * T379: Add UDP broadcast relay support + * mdns repeater scripts - remove python subprocess + * Support setting optional 'type' node in command templates + + -- Christian Poessinger Sat, 06 Jan 2018 13:18:30 +0100 + +vyos-1x (1.0.2) unstable; urgency=low + + * Added mdns-repeater configuration nodes + + -- Christian Poessinger Sat, 09 Dec 2017 10:39:35 +0100 + +vyos-1x (1.0.1) unstable; urgency=low + + * Added the Python library for reading VyOS configs + + -- Daniil Baturin Thu, 17 Aug 2017 22:22:17 -0400 + +vyos-1x (1.0.0) unstable; urgency=low + + * Created the package + + -- Daniil Baturin Thu, 17 Aug 2017 20:17:04 -0400 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..51f3966 --- /dev/null +++ b/debian/control @@ -0,0 +1,39 @@ +Source: vyos-1x +Section: contrib/net +Priority: extra +Maintainer: VyOS Package Maintainers +Build-Depends: debhelper (>= 9), + quilt, + python3, + python3-setuptools, + quilt, + python3-lxml, + python3-nose, + python3-coverage +Standards-Version: 3.9.6 + +Package: vyos-1x +Architecture: all +Depends: python3, + ${python3:Depends}, + python3-netifaces, + python3-jinja2, + python3-pystache, + ipaddrcheck, + tcpdump, + bmon, + hvinfo, + file, + lsscsi, + pciutils, + usbutils, + snmp, snmpd, + openssh-server, + ntp, + iputils-arping, + libvyosconfig0, + beep, + ${shlibs:Depends}, + ${misc:Depends} +Description: VyOS configuration scripts and data + VyOS configuration scripts, interface definitions, and everything diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..20704c4 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,35 @@ +This package was debianized by Daniil Baturin on +Thu, 17 Aug 2017 20:17:04 -0400 + +It's original content from the GIT repository + +Upstream Author: + + + +Copyright: + + 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 General Public License as published by +the Free Software Foundation; either version 2, 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. + +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/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) 2017, Daniil Baturin and +is licensed under the GPL, see above. diff --git a/debian/lintian-overrides b/debian/lintian-overrides new file mode 100644 index 0000000..6c5d671 --- /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 new file mode 100755 index 0000000..d284471 --- /dev/null +++ b/debian/rules @@ -0,0 +1,61 @@ +#!/usr/bin/make -f + +DIR := debian/vyos-1x +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 + +MIGRATION_SCRIPTS_DIR := /opt/vyatta/etc/config-migrate/migrate/ + +%: + dh $@ --with python3, --with quilt + +override_dh_auto_build: + make all + +override_dh_auto_install: + dh_install -pvyos-1x + 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 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) diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml new file mode 100644 index 0000000..0437192 --- /dev/null +++ b/interface-definitions/bcast-relay.xml @@ -0,0 +1,68 @@ + + + + + + + + UDP Broadcast Relay parameters + + + + + Unique ID for each UDP port to forward + + 1-99 + Numerical ID # + + 990 + + + + + + + + Set source IP of forwarded packets, otherwise original senders address is used + + ipv4 + Optional source address for forwarded packets + + + + + + + + + Description + + + + + Interface to repeat UDP broadcasts to [REQUIRED] + + + + + + + + + Destination or source port to listen and retransmit on [REQUIRED] + + 1-65535 + UDP port to listen on + + + + + + + + + + + + + diff --git a/interface-definitions/beep-on-boot.xml b/interface-definitions/beep-on-boot.xml new file mode 100644 index 0000000..0da7d0d --- /dev/null +++ b/interface-definitions/beep-on-boot.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + plays sound via system speaker when you can login + + 9999 + + + + + + + diff --git a/interface-definitions/cron.xml b/interface-definitions/cron.xml new file mode 100644 index 0000000..2d4921b --- /dev/null +++ b/interface-definitions/cron.xml @@ -0,0 +1,75 @@ + + + + + + + + + + Task scheduler settings + + + + + Scheduled task + + <string> + Task name + + 999 + + + + + UNIX crontab time specification string + + + + + Execution interval + + <minutes> + Execution interval in minutes + + + <minutes>m + Execution interval in minutes + + + <hours>h + Execution interval in hours + + + <days>d + Execution interval in days + + + [1-9]([0-9]*)([mhd]{0,1}) + + + + + + Executable path and arguments + + + + + Path to executable + + + + + Arguments passed to the executable + + + + + + + + + + + diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml new file mode 100644 index 0000000..a00b23d --- /dev/null +++ b/interface-definitions/dns-forwarding.xml @@ -0,0 +1,135 @@ + + + + + + + + + + DNS forwarding + 918 + + + + + DNS forwarding cache size + + 0-10000 + DNS forwarding cache size + + + + + + + + + Use DNS servers received from DHCP server for specified interface + + + + + + + + + Domain to forward to a custom DNS server + + + + + Domain Name Server (DNS) to forward queries to + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + + + + + Do not use local /etc/hosts file in name resolution + + + + + + Addresses to listen for DNS queries [REQUIRED] + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + + + Interface to listen for DNS queries [DEPRECATED] + + + + + + + + + Maximum amount of time negative entries are cached + + 0-7200 + Seconds to cache NXDOMAIN entries + + + + + + + + + Domain Name Servers (DNS) addresses + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + + + Use system name servers + + + + + + + + + + diff --git a/interface-definitions/host-name.xml b/interface-definitions/host-name.xml new file mode 100644 index 0000000..bbe6796 --- /dev/null +++ b/interface-definitions/host-name.xml @@ -0,0 +1,26 @@ + + + + + + + + + + System host name (default: vyos) + + [A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9] + + + + + + System domain name + + [A-Za-z0-9][-.A-Za-z0-9]* + + + + + + diff --git a/interface-definitions/mdns-repeater.xml b/interface-definitions/mdns-repeater.xml new file mode 100644 index 0000000..d74e203 --- /dev/null +++ b/interface-definitions/mdns-repeater.xml @@ -0,0 +1,32 @@ + + + + + + + + Multicast DNS (mDNS) parameters + + + + + mDNS repeater configuration + 990 + + + + + Interface to repeat mdns advertisements to [REQUIRED] + + + + + + + + + + + + + diff --git a/interface-definitions/ntp.xml b/interface-definitions/ntp.xml new file mode 100644 index 0000000..d324404 --- /dev/null +++ b/interface-definitions/ntp.xml @@ -0,0 +1,89 @@ + + + + + + + + Network Time Protocol (NTP) configuration + 400 + + + + + Network Time Protocol (NTP) server + + + + + Allow server to be configured even if not reachable + + + + + + Marks the server as unused + + + + + + Specifies the association as preemptable rather than the default persistent + + + + + + Marks the server as preferred + + + + + + + + Network Time Protocol (NTP) server options + + + + + IP address + + ipv4net + IP address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + + + + + + Addresses to listen for NTP queries + + ipv4 + Network Time Protocol (NTP) IPv4 address + + + ipv6 + Network Time Protocol (NTP) IPv6 address + + + + + + + + + + + + + diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml new file mode 100644 index 0000000..103aa39 --- /dev/null +++ b/interface-definitions/snmp.xml @@ -0,0 +1,606 @@ + + + + + + + + Simple Network Management Protocol (SNMP) + 980 + + + + + Community name [REQUIRED] + + ^[a-zA-Z0-9\-_]{1,100} + + Community string is limited to alphanumerical characters only with a total lenght of 100 + + + + + Authorization type (default: 'ro') + + ro + read only + + + rw + read write + + + (ro|rw) + + Authorization type must be either 'rw' or 'ro' + + + + + IP address of SNMP client allowed to contact system + + + + + + + + + + Subnet of SNMP client(s) allowed to contact system + + ipv4net + IP address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + + + + + + Contact information + + .{1,255} + + Contact information is limited to 255 characters or less + + + + + Description information + + .{1,255} + + Description is limited to 255 characters or less + + + + + IP address to listen for incoming SNMP requests + + ipv4 + IPv4 address to listen for incoming SNMP requests + + + ipv6 + IPv6 address to listen for incoming SNMP requests + + + + + + + + + + Port for SNMP service (default: '161') + + 1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + + + + + Location information + + .{1,255} + + Location is limited to 255 characters or less + + + + + Register a subtree for SMUX-based processing + + oid + Object Identifier + + + + + + + SNMP trap source address + + + + + + + + + Address of trap target + + + + + + + + + Community used when sending trap information + + + + + Destination port used for trap notification + + 1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + + + + + Simple Network Management Protocol (SNMP) v3 + + + + + Specifies the EngineID that uniquely identify an agent (e.g. 0xff42) + + ^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$ + + ID must contain an even number (from 2 to 36) of hex digits + + + + + Specifies the group with name groupname + + + + + Define group access permission (default: 'ro') + + ro + read only + + + rw + read write + + + (ro|rw) + + Authorization type must be either 'rw' or 'ro' + + + + + Security levels + + noauth + Messages not authenticated and not encrypted (noAuthNoPriv) + + + auth + Messages are authenticated but not encrypted (authNoPriv) + + + priv + Messages are authenticated and encrypted (authPriv) + + + (noauth|auth|priv) + + + + + + Defines the name of view + + service snmp v3 view + + + + + + + + Defines SNMP target for inform or traps for IP + + ipv4 + IP address of trap target + + + ipv6 + IPv6 address of trap target + + + + + + + + + + Defines the privacy + + + + + Defines the encrypted key for authentication + + ^0x[0-9a-f]*$ + + Key must start from '0x' and contain hex digits + + + + + Defines the clear text key for authentication + + ^.{8,}$ + + Key must contain 8 or more characters + + + + + Defines the protocol used for authentication (default: 'md5') + + md5 + Message Digest 5 + + + sha + Secure Hash Algorithm + + + (md5|sha) + + + + + + + + Specifies the EngineID that uniquely identify an agent (e.g. 0xff42) + + ^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$ + + ID must contain from 2 to 36 hex digits + + + + + Specifies TCP/UDP port of destination SNMP traps/informs (default: '162') + + 1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + + + Defines the privacy + + + + + Defines the encrypted key for privacy protocol + + ^0x[0-9a-f]*$ + + Key must start from '0x' and contain hex digits + + + + + Defines the clear text key for privacy protocol + + ^.{8,}$ + + Key must contain 8 or more characters + + + + + Defines the protocol for privacy (default: 'des') + + des + Data Encryption Standard + + + aes + Advanced Encryption Standard + + + (des|aes) + + + + + + + + Defines protocol for notification between TCP and UDP + + tcp + Use Transmission Control Protocol for notifications + + + udp + Use User Datagram Protocol for notifications + + + (tcp|udp) + + + + + + Specifies the type of notification between inform and trap (default: 'inform') + + inform + Use INFORM + + + trap + Use TRAP + + + (inform|trap) + + + + + + Defines username for authentication + + service snmp v3 user + + + + + + + + Specifies that SNMPv3 uses the Transport Security Model (TSM) + + + + + Fingerprint of a TSM server certificate + + ^[0-9A-F]{2}(:[0-9A-F]{2}){19}$ + + Value can be finger print key or filename in /config/snmp/tls/certs + + + + + Defines the port used for TSM (default: '10161') + + 1-65535 + Numeric IP port + + + + + Port number must be in range 1 to 65535 + + + + + + + Specifies the user with name username + + ^[^\(\)\|\-]+$ + + Illegal characters in name + + + + + Specifies the auth + + + + + Defines the encrypted key for authentication + + ^0x[0-9a-f]*$ + + Key must start from '0x' and contain hex digits + + + + + Defines the clear text key for authentication + + ^.{8,}$ + + Key must contain 8 or more characters + + + + + Defines the protocol used for authentication (default: 'md5') + + md5 + Message Digest 5 + + + sha + Secure Hash Algorithm + + + (md5|sha) + + + + + + + + Specifies the EngineID that uniquely identify an agent (e.g. 0xff42) + + ^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$ + + ID must contain from 2 to 36 hex digits + + + + + Specifies group for user name + + service snmp v3 group + + + + + + Define users access permission (default: 'ro') + + ro + read only + + + rw + read write + + + (ro|rw) + + Authorization type must be either 'rw' or 'ro' + + + + + Defines the privacy + + + + + Defines the encrypted key for privacy protocol + + ^0x[0-9a-f]*$ + + Key must start from '0x' and contain hex digits + + + + + Defines the clear text key for privacy protocol + + ^.{8,}$ + + Key must contain 8 or more characters + + + + + Defines the protocol for privacy (default: 'des') + + des + Data Encryption Standard + + + aes + Advanced Encryption Standard + + + (des|aes) + + + + + + Specifies finger print or file name of TSM certificate + + + + + + + + + Specifies the view with name viewname + + ^[^\(\)\|\-]+$ + + Illegal characters in name + + + + + Specifies the oid + + ^[0-9]+(\\.[0-9]+)*$ + + OID must start from a number + + + + + Exclude is an optional argument + + + + + Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant + + ^[0-9a-f]{2}([\\.:][0-9a-f]{2})*$ + + MASK is a list of hex octets, separated by '.' or ':' + + + + + + + + + + + + + diff --git a/interface-definitions/ssh.xml b/interface-definitions/ssh.xml new file mode 100644 index 0000000..9b3a2fd --- /dev/null +++ b/interface-definitions/ssh.xml @@ -0,0 +1,157 @@ + + + + + + + + + + Secure SHell (SSH) protocol + 500 + + + + + SSH user/group access controls. Directives are processed in this order: deny-users, allow-users, deny-groups and allow-groups + + + + + + + Allow members of a group to login + + + + + + Allow specific users to login + + + + + + + + + + Disallow members of a group to login + + + + + + Disallow specific users to login + + + + + + + + + + Allow the root user to login + + + + + + Allowed ciphers + + + + + + + + + Don't validate the remote host name with DNS + + + + + + Disable password-based authentication + + + + + + Allowed key exchange (KEX) algorithms + + + + + + + + + Local addresses SSH service should listen on + + ipv4 + IP address to listen for incoming connections + + + ipv6 + IPv6 address to listen for incoming connections + + + + + + + + + + + Log level + + QUIET + stay silent + + + FATAL + log fatals only + + + ERROR + log errors and fatals only + + + INFO + default log level + + + VERBOSE + enable logging of failed login attempts + + + + + + Allowed message authentication code (MAC) algorithms + + + + + + + + + Port for SSH service + + 1-65535 + Numeric IP port + + + + + + + + + + + diff --git a/op-mode-definitions/bandwidth-monitor.xml b/op-mode-definitions/bandwidth-monitor.xml new file mode 100644 index 0000000..410ab49 --- /dev/null +++ b/op-mode-definitions/bandwidth-monitor.xml @@ -0,0 +1,23 @@ + + + + + + + Monitor interface bandwidth in real time + + + + bmon -p $4 + + Monitor bandwidth usage on specified interface + + + + + + + + + + diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml new file mode 100644 index 0000000..e789f4a --- /dev/null +++ b/op-mode-definitions/dns-forwarding.xml @@ -0,0 +1,72 @@ + + + + + + + + + + Show DNS forwarding information + + + + + Show DNS forwarding statistics + + sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py + + + + + + + + + + + + Restart a DNS service + + + + + Restart DNS forwarding service + + sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh + + + + + + + + + + Reset a DNS service state + + + + + Reset DNS forwarding cache + + + + sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py $5 + + Reset DNS forwarding cache for a domain + + + + sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py --all + + Reset DNS forwarding cache + + + + + + + + + diff --git a/op-mode-definitions/poweroff.xml b/op-mode-definitions/poweroff.xml new file mode 100644 index 0000000..07cea79 --- /dev/null +++ b/op-mode-definitions/poweroff.xml @@ -0,0 +1,40 @@ + + + + + Poweroff the system + + /opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff + + + + Poweroff the system without confirmation + + /opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff --now + + + + + Cancel a pending poweroff + + /opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff_cancel + + + + + + Poweroff at a specific time + + HH:MM + MMDDYY + midnight + noon + + + /opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff_at --at_time '$3' + + + + + + diff --git a/op-mode-definitions/reboot.xml b/op-mode-definitions/reboot.xml new file mode 100644 index 0000000..2c5a85d --- /dev/null +++ b/op-mode-definitions/reboot.xml @@ -0,0 +1,40 @@ + + + + + Reboot the system + + /opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot + + + + Reboot the system without confirmation + + /opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot --now + + + + + Cancel a pending reboot + + /opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot_cancel + + + + + + Reboot at a specific time + + HH:MM + MMDDYY + midnight + noon + + + /opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot_at --at_time '$3' + + + + + + diff --git a/op-mode-definitions/show-arp.xml b/op-mode-definitions/show-arp.xml new file mode 100644 index 0000000..92c231c --- /dev/null +++ b/op-mode-definitions/show-arp.xml @@ -0,0 +1,25 @@ + + + + + + + Show Address Resolution Protocol (ARP) information + + /usr/sbin/arp -e -n + + + + Show Address Resolution Protocol (ARP) cache for specified interface + + + + + /usr/sbin/arp -e -n -i '$4' + + + + + + + diff --git a/op-mode-definitions/show-bridge.xml b/op-mode-definitions/show-bridge.xml new file mode 100644 index 0000000..a324195 --- /dev/null +++ b/op-mode-definitions/show-bridge.xml @@ -0,0 +1,37 @@ + + + + + + + Show bridging information + + /sbin/brctl show + + + + Show bridge information for a given bridge interface + + + + + /sbin/brctl show '$3' + + + + Show bridge Media Access Control (MAC) address table + + /sbin/brctl showmacs '$3' + + + + Show bridge spanning tree information + + /sbin/brctl showstp '$3' + + + + + + + diff --git a/op-mode-definitions/show-configuration.xml b/op-mode-definitions/show-configuration.xml new file mode 100644 index 0000000..0b1507b --- /dev/null +++ b/op-mode-definitions/show-configuration.xml @@ -0,0 +1,39 @@ + + + + + + + Show available saved configurations + + + cli-shell-api showCfg --show-active-only --show-hide-secrets + + + + + Show running configuration (including default values) + + + cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets + + + + Show running configuration as set commands + + + cli-shell-api showCfg --show-active-only | vyos-config-to-commands + + + + Show available saved configurations + + + ${vyos_op_scripts_dir}/show-configuration-files.sh + + + + + + + diff --git a/op-mode-definitions/show-date.xml b/op-mode-definitions/show-date.xml new file mode 100644 index 0000000..705172b --- /dev/null +++ b/op-mode-definitions/show-date.xml @@ -0,0 +1,30 @@ + + + + + + + Show system time and date + + /bin/date + + + + Show system date and time as Coordinated Universal Time + + /bin/date -u + + + + Show UTC date in Maya calendar format + + ${vyos_op_scripts_dir}/maya_date.py $(date +%s) + + + + + + + + + diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml new file mode 100644 index 0000000..db47395 --- /dev/null +++ b/op-mode-definitions/show-disk.xml @@ -0,0 +1,24 @@ + + + + + + + Show status of disk device + + + + + + + + Show disk drive formatting + + ${vyos_op_scripts_dir}/show-disk-format.sh $3 + + + + + + + diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml new file mode 100644 index 0000000..6cd912a --- /dev/null +++ b/op-mode-definitions/show-hardware.xml @@ -0,0 +1,95 @@ + + + + + + + Show system hardware details + + + + + Show CPU info + + lscpu + + + + Show system CPU details + + cat /proc/cpuinfo + + + + Show CPU's on system + + ${vyos_op_scripts_dir}/cpu_summary.py + + + + + + + Show system DMI details + + ${vyatta_bindir}/vyatta-show-dmi + + + + + Show system RAM details + + cat /proc/meminfo + + + + + Show system PCI bus details + + lspci + + + + Show verbose system PCI bus details + + lspci -vvv + + + + + + + + Show SCSI device information + + lsscsi + + + + Show detailed SCSI device information + + lsscsi -vvv + + + + + + + Show peripherals connected to the USB bus + + lsusb + + + + Show detailed USB bus information + + lsusb -v + + + + + + + + + diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml new file mode 100644 index 0000000..b093074 --- /dev/null +++ b/op-mode-definitions/show-raid.xml @@ -0,0 +1,18 @@ + + + + + + + Show statis of RAID set + + + + + ${vyos_op_scripts_dir}/show_raid.sh $3 + + + + + + diff --git a/op-mode-definitions/show-users.xml b/op-mode-definitions/show-users.xml new file mode 100644 index 0000000..a026e47 --- /dev/null +++ b/op-mode-definitions/show-users.xml @@ -0,0 +1,30 @@ + + + + + + + Show user information + + who -H + + + + Show 10 recently logged in users + + last -aF -n 10 | sed -e 's/^wtmp begins/Displaying logins since/' + + + + Show specified number of recently logged in users + + NUMBER + + + last -aF -n $4 | sed -e 's/^wtmp begins/Displaying logins since/' + + + + + + diff --git a/op-mode-definitions/snmp.xml b/op-mode-definitions/snmp.xml new file mode 100644 index 0000000..a0a47da --- /dev/null +++ b/op-mode-definitions/snmp.xml @@ -0,0 +1,111 @@ + + + + + + + Show status of SNMP on localhost + + + + + Show status of SNMP community + + + + + ${vyos_op_scripts_dir}/snmp.py --community="$4" + + + + Show status of SNMP on remote host + + ${vyos_op_scripts_dir}/snmp.py --community="$4" --host "$6" + + + + + + Show SNMP MIB information + + + + + Show all SNMP interfaces MIB information + + ${vyos_op_scripts_dir}/snmp_ifmib.py + + + + Show SNMP ifAlias for specified interface + + + + + ${vyos_op_scripts_dir}/snmp_ifmib.py --ifalias="$6" + + + + Show SNMP ifDescr for specified interface + + + + + ${vyos_op_scripts_dir}/snmp_ifmib.py --ifdescr="$6" + + + + Show SNMP ifDescr for specified interface + + + + + ${vyos_op_scripts_dir}/snmp_ifmib.py --ifindex="$6" + + + + + + + + Show SNMP v3 status on localhost + + ${vyos_op_scripts_dir}/snmp_v3.py --all + + + + Show TSM certificates + + ${vyos_op_scripts_dir}/snmp_v3_showcerts.sh + + + + Show the list of configured groups + + ${vyos_op_scripts_dir}/snmp_v3.py --group + + + + Show the list of configured targets + + ${vyos_op_scripts_dir}/snmp_v3.py --trap + + + + Show the list of configured users + + ${vyos_op_scripts_dir}/snmp_v3.py --user + + + + Show the list of configured views + + ${vyos_op_scripts_dir}/snmp_v3.py --view + + + + + + + + diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml new file mode 100644 index 0000000..a681064 --- /dev/null +++ b/op-mode-definitions/traffic-dump.xml @@ -0,0 +1,45 @@ + + + + + + + Monitor traffic dumps + + + + tcpdump -i $4 + + Monitor traffic dump from an interface + + + + + + + tcpdump -n -i $4 $6 + + Monitor traffic matching filter conditions + + + + tcpdump -n -i $4 -w $6 + + Save traffic dump from an interface to a file + + + + tcpdump -n -i $4 -w $6 $8 + + Save a dump of traffic matching filter conditions to a file + + + + + + + + + + + diff --git a/op-mode-definitions/version.xml b/op-mode-definitions/version.xml new file mode 100644 index 0000000..593785f --- /dev/null +++ b/op-mode-definitions/version.xml @@ -0,0 +1,27 @@ + + + + + + + Show system version information + + ${vyos_op_scripts_dir}/version.py + + + + Show system version and some fun stuff + + ${vyos_op_scripts_dir}/version.py --funny + + + + Show system version and versions of all packages + + ${vyos_op_scripts_dir}/version.py --all + + + + + + diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..304ea5c --- /dev/null +++ b/python/setup.py @@ -0,0 +1,21 @@ +import os +from setuptools import setup + +setup( + name = "vyos", + version = "1.2.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=['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 0000000..9b5ed21 --- /dev/null +++ b/python/vyos/__init__.py @@ -0,0 +1 @@ +from .base import * diff --git a/python/vyos/base.py b/python/vyos/base.py new file mode 100644 index 0000000..4e23714 --- /dev/null +++ b/python/vyos/base.py @@ -0,0 +1,18 @@ +# Copyright 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, see . + + +class ConfigError(Exception): + pass diff --git a/python/vyos/config.py b/python/vyos/config.py new file mode 100644 index 0000000..5af8304 --- /dev/null +++ b/python/vyos/config.py @@ -0,0 +1,416 @@ +# Copyright 2017 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, see . + +""" +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 it is 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. The knowledge of whether in "task Foo" the "tag" is "task" or "Foo" is lost +in time, luckily, 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* 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. + +For this reason, this library has two sets of functions. The base versions, such as ``exists`` or ``return_value`` +are only usable in configuration mode. They take all nodes into account, in both proposed and running configs. +Configuration scripts require access to uncommited changes for obvious reasons. Configuration mode completion helpers +should also use these functions because not having nodes you've just created in completion is annoying. + +However, in operational mode, only the running config is available. Currently, you need to use special functions +for reading it from operational mode scripts, they can be distinguished by the word "effective" in their names. +In the future base versions may be made to detect if they are called from a config session or not. +""" + +import subprocess +import re + + +class VyOSError(Exception): + """ + Raised on config access errors, most commonly if the type of a config tree node + in the system does not match the type of operation. + + """ + pass + + +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): + self._cli_shell_api = "/bin/cli-shell-api" + self._level = "" + + def _make_command(self, op, path): + args = path.split() + cmd = [self._cli_shell_api, op] + args + return cmd + + def _run(self, cmd): + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + out = p.stdout.read() + p.wait() + 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): 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 + self._level = path + " " + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + return(self._level.strip()) + + 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``. + """ + try: + self._run(self._make_command('exists', self._level + path)) + return True + except VyOSError: + return False + + 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 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: + self._run(self._make_command('isMulti', self._level + 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: + self._run(self._make_command('isTag', self._level + 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: + self._run(self._make_command('isLeaf', self._level + path)) + return True + except VyOSError: + return False + + 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 + + Raises: + VyOSError: if node is not a single-value leaf node + + 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``. + """ + full_path = self._level + path + if self.is_multi(path): + raise VyOSError("Cannot use return_value on multi node: {0}".format(full_path)) + elif not self.is_leaf(path): + raise VyOSError("Cannot use return_value on non-leaf node: {0}".format(full_path)) + else: + try: + out = self._run(self._make_command('returnValue', full_path)) + return out + except VyOSError: + return(default) + + 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 + None: if node does not exist + + Raises: + VyOSError: if node is not a multi-value leaf node + + Note: + This function cannot be used outside a configuration session. + In operational mode scripts, use ``return_effective_values``. + """ + full_path = self._level + path + if not self.is_multi(path): + raise VyOSError("Cannot use return_values on non-multi node: {0}".format(full_path)) + elif not self.is_leaf(path): + raise VyOSError("Cannot use return_values on non-leaf node: {0}".format(full_path)) + else: + try: + out = self._run(self._make_command('returnValues', full_path)) + values = out.split() + return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + except VyOSError: + return(default) + + 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 + + Raises: + VyOSError: if the node is not a tag node + + Note: + There is no way to list all children of a non-tag node in + the current config backend. + + This function cannot be used outside a configuration session. + In operational mode scripts, use ``list_effective_nodes``. + """ + full_path = self._level + path + if self.is_tag(path): + try: + out = self._run(self._make_command('listNodes', full_path)) + values = out.split() + return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + except VyOSError: + return(default) + else: + raise VyOSError("Cannot use list_nodes on a non-tag node: {0}".format(full_path)) + + 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. + """ + try: + self._run(self._make_command('existsEffective', self._level + path)) + return True + except VyOSError: + 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 + + Raises: + VyOSError: if node is not a multi-value leaf node + """ + full_path = self._level + path + if self.is_multi(path): + raise VyOSError("Cannot use return_effective_value on multi node: {0}".format(full_path)) + elif not self.is_leaf(path): + raise VyOSError("Cannot use return_effective_value on non-leaf node: {0}".format(full_path)) + else: + try: + out = self._run(self._make_command('returnEffectiveValue', full_path)) + return out + except VyOSError: + return(default) + + 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 + + Raises: + VyOSError: if node is not a multi-value leaf node + """ + full_path = self._level + path + if not self.is_multi(path): + raise VyOSError("Cannot use return_effective_values on non-multi node: {0}".format(full_path)) + elif not self.is_leaf(path): + raise VyOSError("Cannot use return_effective_values on non-leaf node: {0}".format(full_path)) + else: + try: + out = self._run(self._make_command('returnEffectiveValues', full_path)) + return out + except VyOSError: + return(default) + + 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 + + Raises: + VyOSError: if the node is not a tag node + + Note: + There is no way to list all children of a non-tag node in + the current config backend. + """ + full_path = self._level + path + if self.is_tag(path): + try: + out = self._run(self._make_command('listEffectiveNodes', full_path)) + values = out.split() + return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + except VyOSError: + return(default) + else: + raise VyOSError("Cannot use list_effective_nodes on a non-tag node: {0}".format(full_path)) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py new file mode 100644 index 0000000..4b46a1f --- /dev/null +++ b/python/vyos/configtree.py @@ -0,0 +1,261 @@ +# 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 strip_comments(s): + """ Split a config string into the config section and the trailing comments """ + INITIAL = 0 + IN_COMMENT = 1 + + i = len(s) - 1 + state = INITIAL + + config_end = 0 + + # Find the first character of the comments section at the end, + # if it exists + while (i >= 0): + c = s[i] + + if (state == INITIAL) and re.match(r'\s', c): + # Ignore whitespace + if (i != 0): + i -= 1 + else: + config_end = 0 + break + elif (state == INITIAL) and (c == '/'): + # A comment begins, or it's a stray slash + try: + if (s[i-1] == '*'): + state = IN_COMMENT + i -= 2 + else: + raise ValueError("Invalid syntax") + except: + raise ValueError("Invalid syntax") + elif (state == INITIAL) and (c == '}'): + # We are not inside a comment, that's the end of the last node + config_end = i + 1 + break + elif (state == IN_COMMENT) and (c == '*'): + # A comment ends here + try: + if (s[i-1] == '/'): + state = INITIAL + i -= 2 + except: + raise ValueError("Invalid syntax") + elif (state == IN_COMMENT) and (c != '*'): + # Ignore everything inside comments, including braces + i -= 1 + else: + raise ValueError("Invalid syntax") + + return (s[0:config_end], s[config_end+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.__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.__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.__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, comments_section = strip_comments(config_string) + config = self.__from_string(config_section.encode()) + if config is None: + raise ValueError("Parse error") + else: + self.__config = config + self.__comments = comments_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.__comments) + return config_string + + def to_commands(self): + return self.__to_commands(self.__config).decode() + + def set(self, path, value=None, replace=True): + 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 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/defaults.py b/python/vyos/defaults.py new file mode 100644 index 0000000..ac831c1 --- /dev/null +++ b/python/vyos/defaults.py @@ -0,0 +1,19 @@ +# Copyright 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, see . + + +directories = { + "data": "/usr/share/vyos/" +} diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py new file mode 100644 index 0000000..2e8ee4f --- /dev/null +++ b/python/vyos/interfaces.py @@ -0,0 +1,45 @@ +# Copyright 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, see . + +import re +import json + +import netifaces + + +intf_type_data_file = '/usr/share/vyos/interface-types.json' + +def list_interfaces(): + interfaces = netifaces.interfaces() + + # Remove "fake" interfaces associated with drivers + for i in ["dummy0", "ip6tnl0", "tunl0", "ip_vti0", "ip6_vti0"]: + try: + interfaces.remove(i) + except ValueError: + pass + + return interfaces + +def list_interfaces_of_type(typ): + with open(intf_type_data_file, 'r') as f: + types_data = json.load(f) + + all_intfs = list_interfaces() + if not (typ in types_data.keys()): + raise ValueError("Unknown interface type: {0}".format(typ)) + else: + r = re.compile('^{0}\d+'.format(types_data[typ])) + return list(filter(lambda i: re.match(r, i), all_intfs)) diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py new file mode 100644 index 0000000..97bb5ae --- /dev/null +++ b/python/vyos/limericks.py @@ -0,0 +1,64 @@ +# Copyright 2015, 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, see . + +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. +""" + +] + + +def get_random(): + return limericks[random.randint(0, len(limericks) - 1)] diff --git a/python/vyos/util.py b/python/vyos/util.py new file mode 100644 index 0000000..8b3de79 --- /dev/null +++ b/python/vyos/util.py @@ -0,0 +1,65 @@ +# Copyright 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, see . + +import re + + +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. + """ + 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 diff --git a/python/vyos/version.py b/python/vyos/version.py new file mode 100644 index 0000000..383efbc --- /dev/null +++ b/python/vyos/version.py @@ -0,0 +1,68 @@ +# Copyright 2017 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, see . + +""" +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 + +version_file = os.path.join(vyos.defaults.directories['data'], 'version.json') + +def get_version_data(file=version_file): + """ + Get complete version data + + Args: + file (str): path to the version file + + Returns: + dict: version data + + 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. + """ + with open(file, 'r') as f: + version_data = json.load(f) + return version_data + +def get_version(file=None): + """ + Get the version number + """ + version_data = None + if file: + version_data = get_version_data(file=file) + else: + version_data = get_version_data() + return version_data["version"] diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc new file mode 100644 index 0000000..02175fe --- /dev/null +++ b/schema/interface_definition.rnc @@ -0,0 +1,154 @@ +# interface_definition.rnc: VyConf reference tree XML grammar +# +# Copyright (C) 2014. 2017 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., 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* +} + +# 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 +leafNode = element leafNode +{ + (ownerAttr? & nodeNameAttr), + 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 +# +# 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 })? +} + +# 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. foo bar baz), +# as a configuration path (e.g. interfaces ethernet), +# or as a path to a script file that generates the list (e.g. +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 0000000..195ef27 --- /dev/null +++ b/schema/interface_definition.rng @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc new file mode 100644 index 0000000..9c84de0 --- /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 +# +# 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* +} + +# 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. foo bar baz), +# as a configuration path (e.g. interfaces ethernet), +# or as a path to a script file that generates the list (e.g. +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 0000000..e9e7887 --- /dev/null +++ b/schema/op-mode-definition.rng @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates new file mode 100755 index 0000000..0383c89 --- /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 +# +# 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 listNodes {0}".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 0000000..07e7d52 --- /dev/null +++ b/scripts/build-command-templates @@ -0,0 +1,295 @@ +#!/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 +# +# 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(), 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.py' + 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: + 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) + + 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" + + + 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: + process_node(n, [output_dir]) diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..1258da8 --- /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 0000000..1e34463 --- /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 ' where 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 0000000..3447259 --- /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 +# " v 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 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 0000000..c31cac4 --- /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.sh b/src/completion/list_disks.sh new file mode 100755 index 0000000..f32e558 --- /dev/null +++ b/src/completion/list_disks.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Completion script used by show disks to collect physical disk + +awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' . +# +# + +import sys +import os +import fnmatch +import subprocess + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/udp-broadcast-relay' + +def get_config(): + conf = Config() + conf.set_level("service broadcast-relay id") + relay_id = conf.list_nodes("") + relays = [] + + for id in relay_id: + interface_list = [] + address = conf.return_value("{0} address".format(id)) + description = conf.return_value("{0} description".format(id)) + port = conf.return_value("{0} port".format(id)) + + # split the interface name listing and form a list + if conf.exists("{0} interface".format(id)): + intfs_names = [] + intfs_names = conf.return_values("{0} interface".format(id)) + + for name in intfs_names: + interface_list.append(name) + + relay = { + "id": id, + "address": address, + "description": description, + "interfaces" : interface_list, + "port": port + } + relays.append(relay) + + return relays + +def verify(relays): + for relay in relays: + if not relay["port"]: + raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"])) + + if len(relay["interfaces"]) < 2: + raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"])) + + return None + +def generate(relays): + config_header = '### Autogenerated by bcast_relay.py ###\n' + + config_dir = os.path.dirname(config_file) + config_filename = os.path.basename(config_file) + active_configs = [] + + for config in fnmatch.filter(os.listdir(config_dir), config_filename + '*'): + # determine prefix length to identify service instance + prefix_len = len(config_filename) + active_configs.append(config[prefix_len:]) + + # sort our list + active_configs.sort() + + for id in active_configs[:]: + os.unlink(config_file + id) + + for relay in relays: + file = config_file + str(relay["id"]) + interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) + config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) + + f = open(file, 'w') + f.write(config_header) + if relay["description"]: + f.write('# ' + relay["description"] + '\n') + f.write(config_args) + f.close() + + return None + +def apply(relays): + # first stop all running services + cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}" + os.system(cmd) + + # start only required service instances + for relay in relays: + cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"]) + os.system(cmd) + + 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/beep_if_fully_booted.py b/src/conf_mode/beep_if_fully_booted.py new file mode 100755 index 0000000..f00fcab --- /dev/null +++ b/src/conf_mode/beep_if_fully_booted.py @@ -0,0 +1,42 @@ +#!/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 . +# +# + +import sys +import os + +from vyos.config import Config +from vyos import ConfigError + +def get_config(): + conf = Config() + if not conf.exists('system options beep-if-fully-booted'): + return None + + return True + +def apply(status): + if status is not None: + os.system('/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') + +if __name__ == '__main__': + try: + c = get_config() + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py new file mode 100755 index 0000000..d28e8ff --- /dev/null +++ b/src/conf_mode/dns_forwarding.py @@ -0,0 +1,234 @@ +#!/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 . +# +# + +import sys +import os + +import netifaces +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/powerdns/recursor.conf' + +# 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. +config_tmpl = """ +### Autogenerated by dns_forwarding.py ### + +# Non-configurable defaults +daemon=yes +threads=1 +allow-from=0.0.0.0/0 +log-common-errors=yes +non-local-bind=yes + +# 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-on +local-address={{ listen_on | join(',') }} + +# domain ... server ... +{% if domains -%} + +forward-zones={% for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{{- "," if not loop.last -}} +{% endfor %} + +{% endif %} + +# name-server +forward-zones-recurse=.={{ name_servers | join(';') }} + +""" + +default_config_data = { + 'cache_size': 10000, + 'export_hosts_file': 'yes', + 'listen_on': [], + 'interfaces': [], + 'name_servers': [], + 'negative_ttl': 3600, + 'domains': [] +} + + +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolvers = [] + try: + with open(file, 'r') as resolvconf: + for line in resolvconf.readlines(): + line = line.split('#',1)[0]; + line = line.rstrip(); + if 'nameserver' in line: + resolvers.append(line.split()[1]) + return resolvers + except IOError: + return [] + +def get_config(): + dns = default_config_data + conf = Config() + if not conf.exists('service dns forwarding'): + return None + else: + conf.set_level('service dns forwarding') + + 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 node in conf.list_nodes('domain'): + server = conf.return_values("domain {0} server".format(node)) + domain = { + "name": node, + "servers": server + } + dns['domains'].append(domain) + + if conf.exists('ignore-hosts-file'): + dns['export_hosts_file'] = "no" + + if conf.exists('name-server'): + name_servers = conf.return_values('name-server') + dns['name_servers'] = dns['name_servers'] + name_servers + + if conf.exists('system'): + conf.set_level('system') + system_name_servers = [] + system_name_servers = conf.return_values('name-server') + if not system_name_servers: + print("DNS forwarding warning: No name-servers set under 'system name-server'\n") + else: + dns['name_servers'] = dns['name_servers'] + system_name_servers + conf.set_level('service dns forwarding') + + if conf.exists('listen-address'): + dns['listen_on'] = conf.return_values('listen-address') + + ## Hacks and tricks + + # The old VyOS syntax that comes from dnsmasq was "listen-on $interface". + # pdns wants addresses instead, so we emulate it by looking up all addresses + # of a given interface and writing them to the config + if conf.exists('listen-on'): + print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.") + print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit") + print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.") + print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.") + + interfaces = conf.return_values('listen-on') + + listen4 = [] + listen6 = [] + for interface in interfaces: + try: + addrs = netifaces.ifaddresses(interface) + except ValueError: + print("WARNING: interface {0} does not exist".format(interface)) + continue + + if netifaces.AF_INET in addrs.keys(): + for ip4 in addrs[netifaces.AF_INET]: + listen4.append(ip4['addr']) + + if netifaces.AF_INET6 in addrs.keys(): + for ip6 in addrs[netifaces.AF_INET6]: + listen6.append(ip6['addr']) + + if (not listen4) and (not (listen6)): + print("WARNING: interface {0} has no configured addresses".format(interface)) + + dns['listen_on'] = dns['listen_on'] + listen4 + listen6 + + # Save interfaces in the dict for the reference + dns['interfaces'] = interfaces + + # Add name servers received from DHCP + if conf.exists('dhcp'): + interfaces = [] + interfaces = conf.return_values('dhcp') + for interface in interfaces: + dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface)) + if dhcp_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + + return dns + +def verify(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + if not dns['listen_on']: + raise ConfigError("Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + + if dns['domains']: + for domain in dns['domains']: + if not domain['servers']: + raise ConfigError('Error: No server configured for domain {0}'.format(domain['name'])) + + return None + +def generate(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + + config_text = tmpl.render(dns) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(dns): + if dns is not None: + os.system("systemctl restart pdns-recursor") + else: + # DNS forwarding is removed in the commit + os.system("systemctl stop pdns-recursor") + 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) + sys.exit(1) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py new file mode 100755 index 0000000..3b3958f --- /dev/null +++ b/src/conf_mode/host_name.py @@ -0,0 +1,117 @@ +#!/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 . +# +# + +""" +conf-mode script for 'system host-name' and 'system domain-name'. +""" + +import os +import re +import sys +import subprocess + +from vyos.config import Config +from vyos import ConfigError + + +hosts_file = '/etc/hosts' +hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") +local_addr = '127.0.1.1' # NOSONAR + + +def get_config(): + """Get configuration""" + conf = Config() + + hostname = conf.return_value("system host-name") + domain = conf.return_value("system domain-name") + + # No one likes fixups, but we really don't want VyOS fail to boot + # if hostname is not in the config + if not hostname: + hostname = "vyos" + + if domain: + fqdn = "{0}.{1}".format(hostname, domain) + else: + fqdn = hostname + + return {"hostname": hostname, "domain": domain, "fqdn": fqdn} + + +def verify(config): + """Verify configuration""" + # check for invalid host + + # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" + if not hostname_regex.match(config["hostname"]): + raise ConfigError('Invalid host name ' + config["hostname"]) + + # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" + length = len(config["hostname"]) + if length < 1 or length > 63: + raise ConfigError( + 'Invalid host-name length, must be less than 63 characters') + + return None + + +def generate(config): + """Generate configuration files""" + # read the hosts file + with open(hosts_file, 'r') as f: + hosts = f.read() + + # get the current hostname + old_hostname = subprocess.check_output(['hostname']).decode().strip() + + # replace the local host line + vyos_host_line_re = re.compile(r"({}\s+{}.*)".format(local_addr, old_hostname)) + vyos_host_line = "{}\t{} # VyOS entry\n".format(local_addr, config["fqdn"]) + if re.search(vyos_host_line_re, hosts): + hosts = re.sub(vyos_host_line_re, vyos_host_line, hosts) + else: + # On boot (or after errors), the /etc/hosts file has no line for vyos hostname, + # so we have to add it + hosts = "{0}\n{1}".format(hosts, vyos_host_line) + + with open(hosts_file, 'w') as f: + f.write(hosts) + + return None + + +def apply(config): + """Apply configuration""" + os.system("hostnamectl set-hostname --static {0}".format(config["fqdn"])) + + # restart services that use the hostname + os.system("systemctl restart rsyslog.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/lldp.py b/src/conf_mode/lldp.py new file mode 100644 index 0000000..27749c8 --- /dev/null +++ b/src/conf_mode/lldp.py @@ -0,0 +1,217 @@ +#!/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 . +# +# +# + +import re +import sys + +from vyos.config import Config +from vyos import ConfigError + + +def get_options(config): + options = {} + config.set_level('service lldp') + options['listen_vlan'] = config.exists('listen-vlan') + + options["addr"] = config.return_value('management-address') + + snmp = config.exists('snmp enable') + options["snmp"] = snmp + if snmp: + config.set_level('') + options["sys_snmp"] = config.exists('service snmp') + config.set_level('service lldp') + + config.set_level('service lldp legacy-protocols') + options["cdp"] = config.exists("cdp") + options["edp"] = config.exists("edp") + options["fdp"] = config.exists("fdp") + options["sonmp"] = config.exists("sonmp") + return options + + +def get_interface_list(config): + config.set_level('service lldp') + intfs_names = config.list_nodes('interface') + if len(intfs_names) < 0: + return 0 + interface_list = [] + for name in intfs_names: + config.set_level("service lldp interface {0}".format(name)) + disable = config.exists('disable') + intf = { + "name": name, + "disable": disable + } + interface_list.append(intf) + return interface_list + + +def get_location_intf(config, name): + path = "service lldp interface {0}".format(name) + config.set_level(path) + if config.exists("location"): + return 0 + config.set_level("{} location".format(path)) + civic_based = {} + elin = None + coordinate_based = {} + + if config.exists('civic-based'): + config.set_level("{} location civic-based".format(path)) + cc = config.return_value("country-code") + civic_based["country_code"] = cc + civic_based["ca_type"] = [] + ca_types_names = config.list_nodes('ca-type') + if ca_types_names: + for ca_types_name in ca_types_names: + config.set_level("{0} location civic-based ca-type {1}".format(path, ca_types_name)) + ca_val = config.return_value('ca-value') + ca_type = { + "name": ca_types_name, + "ca_val": ca_val + } + civic_based["ca_type"].append(ca_type) + + elif config.exists("elin"): + elin = config.return_value("elin") + + elif config.exists("coordinate-based"): + config.set_level("{} location coordinate-based".format(path)) + alt = config.return_value("altitude") + lat = config.return_value("latitude") + long = config.return_value("longitude") + datum = config.return_value("datum") + coordinate_based["altitude"] = alt + coordinate_based["latitude"] = lat + coordinate_based["longitude"] = long + coordinate_based["datum"] = datum + + intf = { + "name": name, + "civic_based": civic_based, + "elin": elin, + "coordinate_based": coordinate_based + + } + return intf + + +def get_location(config): + config.set_level('service lldp') + 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(): + conf = Config() + options = get_options(conf) + interface_list = get_interface_list(conf) + location = get_location(conf) + lldp = {"options": options, "interface_list": interface_list, "location": location} + return lldp + + +def verify(lldp): + + # check location + for location in lldp["location"]: + + # check civic-based + if len(location["civic_based"]) > 0: + if len(location["coordinate_based"]) > 0 or location["elin"]: + raise ConfigError("Can only configure 1 location type for interface {0}".format(location["name"])) + + # check country-code + if not location["civic_based"]["country_code"]: + raise ConfigError("Invalid location for interface {0}: must configure the country code".format(location["name"])) + + if not re.match(r"^[a-zA-Z]{2}$", location["civic_based"]["country_code"]): + raise ConfigError("Invalid location for interface {0}: country-code must be 2 characters".format(location["name"])) + # check ca-type + if len(location["civic_based"]["ca_type"]) < 0: + raise ConfigError("Invalid location for interface {0}: must define at least 1 ca-type".format(location["name"])) + + for ca_type in location["civic_based"]["ca_type"]: + if not int(ca_type["name"]) in range(0, 129): + raise ConfigError("Invalid location for interface {0}: ca-type must between 0-128".format(location["name"])) + + if not ca_type["ca_val"]: + raise ConfigError("Invalid location for interface {0}: must configure the ca-value for ca-type {1}".format(location["name"],ca_type["name"])) + + # check coordinate-based + elif 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}: 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}: 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}: 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}: datum should be WGS84, NAD83, or MLLW".format(location["name"])) + + # check elin + elif len(location["elin"]) > 0: + if not re.match(r"^[0-9]{10,25}$", location["elin"]): + raise ConfigError("Invalid location for interface {0}: 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(config): + pass + + +def apply(config): + 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/mdns_repeater.py b/src/conf_mode/mdns_repeater.py new file mode 100755 index 0000000..474a6a5 --- /dev/null +++ b/src/conf_mode/mdns_repeater.py @@ -0,0 +1,93 @@ +#!/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 . +# +# + +import sys +import os + +import netifaces + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/mdns-repeater' + +def get_config(): + interface_list = [] + + conf = Config() + conf.set_level('service mdns repeater') + if not conf.exists(''): + return interface_list + + if conf.exists('interface'): + intfs_names = [] + intfs_names = conf.return_values('interface') + + for name in intfs_names: + interface_list.append(name) + + return interface_list + +def verify(mdns): + # '0' interfaces are possible, think of service deletion. Only '1' is not supported! + if len(mdns) == 1: + raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns)) + + # For mdns-repeater to work it is essential that the interfaces + # have an IP address assigned + for intf in mdns: + try: + netifaces.ifaddresses(intf)[netifaces.AF_INET] + except KeyError as e: + raise ConfigError('No IP address configured for interface "%s"!' % intf) + + return None + +def generate(mdns): + config_header = '### Autogenerated by mdns_repeater.py ###\n' + if len(mdns) > 0: + config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n' + else: + config_args = 'DAEMON_ARGS=""\n' + + # write new configuration file + f = open(config_file, 'w') + f.write(config_header) + f.write(config_args) + f.close() + + return None + +def apply(mdns): + if len(mdns) == 0: + cmd = "sudo systemctl stop mdns-repeater" + else: + cmd = "sudo systemctl restart mdns-repeater" + + os.system(cmd) + 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/ntp.py b/src/conf_mode/ntp.py new file mode 100755 index 0000000..2a60885 --- /dev/null +++ b/src/conf_mode/ntp.py @@ -0,0 +1,173 @@ +#!/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 . +# +# + +import sys +import os + +import jinja2 +import ipaddress + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ntp.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### 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 ignore +# Local users have unrestricted access, allowing reconfiguration via ntpdc +restrict 127.0.0.1 +restrict -6 ::1 + + +# +# Configurable section +# + +{% if servers -%} +{% for s in servers -%} +# Server configuration for: {{ s.name }} +server {{ s.name }} iburst {{ s.options | join(" ") }} + +{% endfor -%} +{% endif %} + +{% if allowed_networks -%} +{% for n in allowed_networks -%} +# Client configuration for network: {{ n.network }} +restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer + +{% endfor -%} +{% endif %} + +{% if listen_address -%} +# NTP should listen on configured addresses only +interface ignore wildcard +{% for a in listen_address -%} +interface listen {{ a }} +{% endfor -%} +{% endif %} + +""" + +default_config_data = { + 'servers': [], + 'allowed_networks': [], + 'listen_address': [] +} + +def get_config(): + ntp = default_config_data + conf = Config() + if not conf.exists('system ntp'): + return None + else: + conf.set_level('system ntp') + + if conf.exists('allow-clients address'): + networks = conf.return_values('allow-clients address') + for n in networks: + addr = ipaddress.ip_network(n) + net = { + "network" : n, + "address" : addr.network_address, + "netmask" : addr.netmask + } + + ntp['allowed_networks'].append(net) + + if conf.exists('listen-address'): + ntp['listen_address'] = conf.return_values('listen-address') + + if conf.exists('server'): + for node in conf.list_nodes('server'): + options = [] + server = { + "name": node, + "options": [] + } + if conf.exists('server {0} dynamic'.format(node)): + options.append('dynamic') + if conf.exists('server {0} noselect'.format(node)): + options.append('noselect') + if conf.exists('server {0} preempt'.format(node)): + options.append('preempt') + if conf.exists('server {0} prefer'.format(node)): + options.append('prefer') + + server['options'] = options + ntp['servers'].append(server) + + return ntp + +def verify(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + # Configuring allowed clients without a server makes no sense + if len(ntp['allowed_networks']) and not len(ntp['servers']): + raise ConfigError('NTP server not configured') + + for n in ntp['allowed_networks']: + try: + addr = ipaddress.ip_network( n['network'] ) + break + except ValueError: + raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network'])) + + return None + +def generate(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ntp) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(ntp): + if ntp is not None: + os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload') + else: + # NTP suuport is removed in the commit + os.system('sudo /usr/sbin/invoke-rc.d ntp stop') + 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) + sys.exit(1) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py new file mode 100755 index 0000000..1590e5d --- /dev/null +++ b/src/conf_mode/snmp.py @@ -0,0 +1,804 @@ +#!/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 . +# +# + +import sys +import os +import shutil +import stat +import pwd +import time + +import jinja2 +import ipaddress +import random +import binascii +import re + +import vyos.version + +from vyos.config import Config +from vyos import ConfigError + +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' + +# 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' +} +# SNMPS template - be careful if you edit the template. +client_config_tmpl = """ +### Autogenerated by snmp.py ### +{% if trap_source -%} +clientaddr {{ trap_source }} +{% endif %} + +""" + +# SNMPS template - be careful if you edit the template. +access_config_tmpl = """ +### Autogenerated by snmp.py ### +{% if v3_users %} +{% for u in v3_users %} +{{ u.mode }}user {{ u.name }} +{% endfor %} +{% endif -%} +rwuser {{ vyos_user }} + +""" + +# SNMPS template - be careful if you edit the template. +user_config_tmpl = """ +### Autogenerated by snmp.py ### +# user +{% if v3_users %} +{% for u in v3_users %} +{% if u.authOID == 'none' %} +createUser {{ u.name }} +{% elif u.authPassword %} +createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} +{% else %} +usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x +{% endif %} +{% endfor %} +{% endif %} + +createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +""" + +# SNMPS template - be careful if you edit the template. +daemon_config_tmpl = """ +### Autogenerated by snmp.py ### + +# non configurable defaults +sysObjectID 1.3.6.1.4.1.44641 +sysServices 14 +master agentx +agentXPerms 0755 0755 +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 +smuxpeer .1.3.6.1.4.1.3317.1.2.2 +smuxpeer .1.3.6.1.4.1.3317.1.2.3 +smuxpeer .1.3.6.1.4.1.3317.1.2.5 +smuxpeer .1.3.6.1.4.1.3317.1.2.8 +smuxpeer .1.3.6.1.4.1.3317.1.2.9 +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 # +######################## + +{% if v3_tsm_key %} +[snmp] localCert {{ v3_tsm_key }} +{% endif %} + +# 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,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %} + + +# SNMP communities +{% if communities -%} +{% for c in communities %} +{% if c.network -%} +{% for network in c.network %} +{{ c.authorization }}community {{ c.name }} {{ network }} +{% endfor %} +{% else %} +{{ c.authorization }}community {{ c.name }} +{% endif %} +{% endfor %} +{% endif %} + +{% 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 t in trap_targets %} +trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} +{% endfor %} +{% endif %} + +# +# SNMPv3 stuff goes here +# +{% if v3_enabled %} + +# views +{% if v3_views -%} +{% for v in v3_views %} +{% for oid in v.oids %} +view {{ v.name }} included .{{ oid.oid }} +{% endfor %} +{% endfor %} +{% endif %} + +# access +# context sec.model sec.level match read write notif +{% if v3_groups -%} +{% for g in v3_groups %} +{% if g.mode == 'ro' %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none +{% elif g.mode == 'rw' %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none +{% endif %} +{% endfor -%} +{% endif %} + +# trap-target +{% if v3_traps -%} +{% for t in v3_traps %} +trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.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 -%} +{% endif %} + +# group +{% if v3_users -%} +{% for u in v3_users %} +group {{ u.group }} usm {{ u.name }} +group {{ u.group }} tsm {{ u.name }} +{% endfor %} +{% endif %} + +{% endif %} + +""" + +default_config_data = { + 'listen_on': [], + 'communities': [], + 'smux_peers': [], + 'location' : '', + 'description' : '', + 'contact' : '', + 'trap_source': '', + 'trap_targets': [], + 'vyos_user': '', + 'vyos_user_pass': '', + 'version': '999', + 'v3_enabled': 'False', + 'v3_engineid': '', + 'v3_groups': [], + 'v3_traps': [], + 'v3_tsm_key': '', + 'v3_tsm_port': '10161', + 'v3_users': [], + 'v3_views': [] +} + +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: + conf.set_level('service snmp') + + version_data = vyos.version.get_version_data() + snmp['version'] = version_data['version'] + + # create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' + # os.urandom(8) returns 8 bytes of random data + snmp['vyos_user'] = 'vyatta' + binascii.hexlify(os.urandom(8)).decode('utf-8') + snmp['vyos_user_pass'] = binascii.hexlify(os.urandom(16)).decode('utf-8') + + if conf.exists('community'): + for name in conf.list_nodes('community'): + community = { + 'name': name, + 'authorization': 'ro', + 'network': [] + } + + if conf.exists('community {0} authorization'.format(name)): + community['authorization'] = conf.return_value('community {0} authorization'.format(name)) + + if conf.exists('community {0} network'.format(name)): + community['network'] = conf.return_values('community {0} network'.format(name)) + + 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'): + listen = '' + port = '161' + if conf.exists('listen-address {0} port'.format(addr)): + port = conf.return_value('listen-address {0} port'.format(addr)) + + if ipaddress.ip_address(addr).version == 4: + # udp:127.0.0.1:161 + listen = 'udp:' + addr + ':' + port + elif ipaddress.ip_address(addr).version == 6: + # udp6:[::1]:161 + listen = 'udp6:' + '[' + addr + ']' + ':' + port + else: + raise ConfigError('Invalid IP address version') + + snmp['listen_on'].append(listen) + + 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) + + ######################################################################### + # ____ _ _ __ __ ____ _____ # + # / ___|| \ | | \/ | _ \ __ _|___ / # + # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # + # ___) | |\ | | | | __/ \ 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, + 'engineID': '', + 'secName': '', + 'authProtocol': 'md5', + 'authPassword': '', + 'authMasterKey': '', + 'privProtocol': 'des', + 'privPassword': '', + 'privMasterKey': '', + 'ipProto': 'udp', + 'ipPort': '162', + 'type': '', + 'secLevel': 'noAuthNoPriv' + } + + if conf.exists('v3 trap-target {0} engineid'.format(trap)): + # Set the context engineID used for SNMPv3 REQUEST messages scopedPdu. + # If not specified, this will default to the authoritative engineID. + trap_cfg['engineID'] = conf.return_value('v3 trap-target {0} engineid'.format(trap)) + + 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-key'.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-key'.format(trap)) + + if conf.exists('v3 trap-target {0} auth encrypted-key'.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-key'.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-key'.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-key'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy encrypted-key'.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-key'.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 tsm' + # + if conf.exists('v3 tsm'): + if conf.exists('v3 tsm local-key'): + snmp['v3_tsm_key'] = conf.return_value('v3 tsm local-key') + + if conf.exists('v3 tsm port'): + snmp['v3_tsm_port'] = conf.return_value('v3 tsm port') + + # + # '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': '', + 'authOID': 'none', + 'engineID': '', + 'group': '', + 'mode': 'ro', + 'privMasterKey': '', + 'privPassword': '', + 'privOID': '', + 'privTsmKey': '', + 'privProtocol': '' + } + + # + # v3 user {0} auth + # + if conf.exists('v3 user {0} auth encrypted-key'.format(user)): + user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user)) + + if conf.exists('v3 user {0} auth plaintext-key'.format(user)): + user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-key'.format(user)) + + if conf.exists('v3 user {0} auth type'.format(user)): + type = conf.return_value('v3 user {0} auth type'.format(user)) + user_cfg['authProtocol'] = type + user_cfg['authOID'] = OIDs[type] + + # + # v3 user {0} engineid + # + if conf.exists('v3 user {0} engineid'.format(user)): + user_cfg['engineID'] = conf.return_value('v3 user {0} engineid'.format(user)) + + # + # 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-key'.format(user)): + user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user)) + + if conf.exists('v3 user {0} privacy plaintext-key'.format(user)): + user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user)) + + if conf.exists('v3 user {0} privacy tsm-key'.format(user)): + user_cfg['privTsmKey'] = conf.return_value('v3 user {0} privacy tsm-key'.format(user)) + + if conf.exists('v3 user {0} privacy type'.format(user)): + type = conf.return_value('v3 user {0} privacy type'.format(user)) + 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: + return None + + # bail out early if SNMP v3 is not configured + if not snmp['v3_enabled']: + return None + + tsmKeyPattern = re.compile('^[0-9A-F]{2}(:[0-9A-F]{2}){19}$', re.IGNORECASE) + + if snmp['v3_tsm_key']: + if not tsmKeyPattern.match(snmp['v3_tsm_key']): + if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): + if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): + raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + + 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-key/plaintext-key for trap auth') + + if trap['authPassword'] == '' and trap['authMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for trap auth') + + if trap['privPassword'] and trap['privMasterKey']: + raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap privacy') + + if trap['privPassword'] == '' and trap['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-key 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 'type' in trap.keys(): + if trap['type'] == 'trap' and trap['engineID'] == '': + raise ConfigError('must specify engineid if type is "trap"') + else: + raise ConfigError('"type" 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 + # + error = True + if user['group']: + 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 seclevel is 'auth' or seclevel is 'priv': + if user['authPassword'] and user['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') + + if user['authPassword'] == '' and user['authMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for user auth') + + if user['authProtocol'] == '': + raise ConfigError('Must specify auth type') + + # seclevel 'priv' is more restrictive + if seclevel is 'priv': + if user['privPassword'] and user['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') + + if user['privPassword'] == '' and user['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') + + if user['privMasterKey'] and user['engineID'] == '': + raise ConfigError('Can not have "encrypted-key" without engineid') + + if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '': + raise ConfigError('Must specify auth or tsm-key for user auth') + + if user['privProtocol'] == '': + raise ConfigError('Must specify privacy type') + + if user['mode'] == '': + raise ConfigError('Must specify user mode ro/rw') + + if user['privTsmKey']: + if not tsmKeyPattern.match(snmp['v3_tsm_key']): + if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): + if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): + raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + + 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 + os.system("sudo systemctl stop snmpd.service") + rmfile(config_file_client) + rmfile(config_file_daemon) + rmfile(config_file_access) + rmfile(config_file_user) + + if snmp is None: + return None + + # Write client config file + tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_client, 'w') as f: + f.write(config_text) + + # Write server config file + tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_daemon, 'w') as f: + f.write(config_text) + + # Write access rights config file + tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_access, 'w') as f: + f.write(config_text) + + # Write access rights config file + tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_user, 'w') as f: + f.write(config_text) + + return None + +def apply(snmp): + if snmp is not None: + + nonvolatiledir = '/config/snmp/tls' + volatiledir = '/etc/snmp/tls' + if not os.path.exists(nonvolatiledir): + os.makedirs(nonvolatiledir) + os.chmod(nonvolatiledir, stat.S_IWUSR | stat.S_IRUSR) + # get uid for user 'snmp' + snmp_uid = pwd.getpwnam('snmp').pw_uid + os.chown(nonvolatiledir, snmp_uid, -1) + + # move SNMP certificate files from volatile location to non volatile /config/snmp + if os.path.exists(volatiledir) and os.path.isdir(volatiledir): + files = os.listdir(volatiledir) + for f in files: + shutil.move(volatiledir + '/' + f, nonvolatiledir) + os.chmod(nonvolatiledir + '/' + f, stat.S_IWUSR | stat.S_IRUSR) + + os.rmdir(volatiledir) + os.symlink(nonvolatiledir, volatiledir) + + if os.path.islink(volatiledir): + link = os.readlink(volatiledir) + if link != nonvolatiledir: + os.unlink(volatiledir) + os.symlink(nonvolatiledir, volatiledir) + + # start SNMP daemon + os.system("sudo systemctl restart snmpd.service") + + # the passwords are not available immediately so this is a workaround + # and should be changed to polling + time.sleep(2) + + # Back in the Perl days the configuration was re-read and any + # plaintext password inside the configuration was replaced by + # the encrypted one which can be found in 'config_file_user' + with open(config_file_user, 'r') as f: + engineID = '' + for line in f: + if line.startswith('oldEngineID'): + string = line.split(' ') + engineID = string[1] + + if line.startswith('usmUser'): + string = line.split(' ') + cfg = { + 'user': string[4].replace(r'"', ''), + 'auth_pw': string[8], + 'priv_pw': string[10] + } + # No need to take care about the VyOS internal user + if cfg['user'] == snmp['vyos_user']: + continue + + # Now update the running configuration + # + # Currently when executing os.system() the environment does not have the vyos_libexec_dir variable set, see T685 + os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" engineid {1} > /dev/null'.format(cfg['user'], engineID)) + os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key {1} > /dev/null'.format(cfg['user'], cfg['auth_pw'])) + os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key {1} > /dev/null'.format(cfg['user'], cfg['priv_pw'])) + os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) + os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + + 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/ssh.py b/src/conf_mode/ssh.py new file mode 100755 index 0000000..f1ac194 --- /dev/null +++ b/src/conf_mode/ssh.py @@ -0,0 +1,255 @@ +#!/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 . +# +# + +import sys +import os + +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ssh/sshd_config' + +# Please be careful if you edit the template. +config_tmpl = """ + +### Autogenerated by ssh.py ### + +# 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 +UsePrivilegeSeparation yes +KeyRegenerationInterval 3600 +ServerKeyBits 1024 +SyslogFacility AUTH +LoginGraceTime 120 +StrictModes yes +RSAAuthentication yes +PubkeyAuthentication yes +IgnoreRhosts yes +RhostsRSAAuthentication no +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 +HostKey /etc/ssh/ssh_host_key + +# Specifies whether sshd should look up the remote host name, +# and to check that the resolved host name for the remote IP +# address maps back to the very same IP address. +UseDNS {{ host_validation }} + +# Specifies the port number that sshd listens on. The default is 22. +# Multiple options of this type are permitted. +Port {{ port }} + +# Gives the verbosity level that is used when logging messages from sshd +LogLevel {{ log_level }} + +# Specifies whether root can log in using ssh +PermitRootLogin {{ allow_root }} + +# Specifies whether password authentication is allowed +PasswordAuthentication {{ password_authentication }} + +{% if listen_on -%} +# Specifies the local addresses sshd should listen on +{% for a in listen_on -%} +ListenAddress {{ a }} +{% endfor -%} +{% endif %} + +{% if ciphers -%} +# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ +Ciphers {{ ciphers | join(",") }} +{% endif %} + +{% if mac -%} +# Specifies the available MAC (message authentication code) algorithms. The MAC +# algorithm is used for data integrity protection. Multiple algorithms must be +# comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ +MACs {{ mac | join(",") }} +{% endif %} + +{% if key_exchange -%} +# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must +# be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ +KexAlgorithms {{ key_exchange | join(",") }} +{% endif %} + +{% if allow_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# If specified, login is allowed only for user names that match one of the patterns. +# Only user names are valid, a numerical user ID is not recognized. +AllowUsers {{ allow_users | join(" ") }} +{% endif %} + +{% if allow_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# If specified, login is allowed only for users whose primary group or supplementary +# group list matches one of the patterns. Only group names are valid, a numerical group +# ID is not recognized. +AllowGroups {{ allow_groups | join(" ") }} +{% endif %} + +{% if deny_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# Login is disallowed for user names that match one of the patterns. Only user names +# are valid, a numerical user ID is not recognized. +DenyUsers {{ deny_users | join(" ") }} +{% endif %} + +{% if deny_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# Login is disallowed for users whose primary group or supplementary group list matches +# one of the patterns. Only group names are valid, a numerical group ID is not recognized. +DenyGroups {{ deny_groups | join(" ") }} +{% endif %} +""" + +default_config_data = { + 'port' : '22', + 'log_level': 'INFO', + 'allow_root': 'no', + 'password_authentication': 'yes', + 'host_validation': 'yes' +} + +def get_config(): + ssh = default_config_data + conf = Config() + if not conf.exists('service ssh'): + return None + else: + conf.set_level('service ssh') + + if conf.exists('access-control allow user'): + allow_users = conf.return_values('access-control allow user') + ssh['allow_users'] = allow_users + + if conf.exists('access-control allow group'): + allow_groups = conf.return_values('access-control allow group') + ssh['allow_groups'] = allow_groups + + if conf.exists('access-control deny user'): + deny_users = conf.return_values('access-control deny user') + ssh['deny_users'] = deny_users + + if conf.exists('access-control deny group'): + deny_groups = conf.return_values('access-control deny group') + ssh['deny_groups'] = deny_groups + + if conf.exists('allow-root'): + ssh['allow-root'] = 'yes' + + if conf.exists('ciphers'): + ciphers = conf.return_values('ciphers') + ssh['ciphers'] = ciphers + + if conf.exists('disable-host-validation'): + ssh['host_validation'] = 'no' + + if conf.exists('disable-password-authentication'): + ssh['password_authentication'] = 'no' + + if conf.exists('key-exchange'): + kex = conf.return_values('key-exchange') + ssh['key_exchange'] = kex + + if conf.exists('listen-address'): + # We can listen on both IPv4 and IPv6 addresses + # Maybe there could be a check in the future if the configured IP address + # is configured on this system at all? + addresses = conf.return_values('listen-address') + listen = [] + + for addr in addresses: + listen.append(addr) + + ssh['listen_on'] = listen + + if conf.exists('loglevel'): + ssh['log_level'] = conf.return_value('loglevel') + + if conf.exists('mac'): + mac = conf.return_values('mac') + ssh['mac'] = mac + + if conf.exists('port'): + port = conf.return_value('port') + ssh['port'] = port + + return ssh + +def verify(ssh): + if ssh is None: + return None + + if 'loglevel' in ssh.keys(): + allowed_loglevel = 'QUIET, FATAL, ERROR, INFO, VERBOSE' + if not ssh['loglevel'] in allowed_loglevel: + raise ConfigError('loglevel must be one of "{0}"\n'.format(allowed_loglevel)) + + return None + +def generate(ssh): + if ssh is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ssh) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(ssh): + if ssh is not None and 'port' in ssh.keys(): + os.system("sudo systemctl restart ssh") + else: + # SSH access is removed in the commit + os.system("sudo systemctl stop ssh") + 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) + sys.exit(1) diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py new file mode 100755 index 0000000..285afe2 --- /dev/null +++ b/src/conf_mode/task_scheduler.py @@ -0,0 +1,148 @@ +#!/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 . +# +# + +import os +import re +import sys + +from vyos.config import Config +from vyos import ConfigError + + +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(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/helpers/commands-pipe.py b/src/helpers/commands-pipe.py new file mode 100755 index 0000000..1120bb0 --- /dev/null +++ b/src/helpers/commands-pipe.py @@ -0,0 +1,29 @@ +#!/usr/bin/python3 + +import sys +import re + +from signal import signal, SIGPIPE, SIG_DFL +from vyos.configtree import ConfigTree + +signal(SIGPIPE,SIG_DFL) + +config_string = sys.stdin.read().strip() + +if not config_string: + sys.exit(0) + +# When used in conf mode pipe, the config given to the script is likely incomplete +# and breaks the "all top level nodes are neither tag nor leaf" +# invariant, so we wrap it into a fake node. +# Since nodes don't normally start with an underscore, +# __root__ is hygienic enough. +config_string = "__root__ {{ {0} \n }}".format(config_string) + +config_re = re.compile(r'(set|comment)\s+__root__\s+(.*)') + +config = ConfigTree(config_string) +commands = config.to_commands() +commands = config_re.sub("\\1 \\2", commands) + +print(commands) diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py new file mode 100755 index 0000000..d702739 --- /dev/null +++ b/src/helpers/validate-value.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import re +import os +import sys +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--regex', action='append') +parser.add_argument('--exec', action='append') +parser.add_argument('--value', action='store') + +args = parser.parse_args() + +debug = False + +# Multiple arguments work like logical OR + +try: + for r in args.regex: + if re.fullmatch(r, args.value): + sys.exit(0) +except Exception as exn: + if debug: + print(exn) + else: + pass + +try: + for cmd in args.exec: + cmd = "{0} {1}".format(cmd, args.value) + if debug: + print(cmd) + res = os.system(cmd) + if res == 0: + sys.exit(0) +except Exception as exn: + if debug: + print(exn) + else: + pass + +sys.exit(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 0000000..3443591 --- /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/system/7-to-8 b/src/migration-scripts/system/7-to-8 new file mode 100755 index 0000000..4cbb21f --- /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/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py new file mode 100755 index 0000000..7324c75 --- /dev/null +++ b/src/op_mode/cpu_summary.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +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 0000000..da4fba3 --- /dev/null +++ b/src/op_mode/dns_forwarding_reset.py @@ -0,0 +1,49 @@ +#!/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 . +# +# File: vyos-show-version +# Purpose: +# Displays image version and system information. +# Used by the "run show version" command. + + +import os +import sys +import argparse + +import vyos.config + +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 = vyos.config.Config() + if not c.exists_effective('service dns forwarding'): + print("DNS forwarding is not configured") + sys.exit(0) + + if args.all: + os.system("rec_control wipe-cache \'.$\'") + sys.exit(1) + elif args.domain: + os.system("rec_control wipe-cache \'{0}$\'".format(args.domain)) + else: + parser.print_help() + sys.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 0000000..12106fc --- /dev/null +++ b/src/op_mode/dns_forwarding_restart.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if cli-shell-api exists service dns forwarding; then + echo "Restarting the DNS forwarding service" + systemctl restart pdns-recursor +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 0000000..f626244 --- /dev/null +++ b/src/op_mode/dns_forwarding_statistics.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import subprocess +import jinja2 +import sys + +from vyos.config import Config + +PDNS_CMD='/usr/bin/rec_control' + +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") + sys.exit(0) + + data = {} + + data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode() + data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 ) + + tmpl = jinja2.Template(OUT_TMPL_SRC) + print(tmpl.render(data)) diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py new file mode 100755 index 0000000..7d8aefc --- /dev/null +++ b/src/op_mode/maya_date.py @@ -0,0 +1,209 @@ +#!/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 . + +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, + .... + 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/show-configuration-files.sh b/src/op_mode/show-configuration-files.sh new file mode 100755 index 0000000..ad8e074 --- /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-disk-format.sh b/src/op_mode/show-disk-format.sh new file mode 100755 index 0000000..61b15a5 --- /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-raid.sh b/src/op_mode/show-raid.sh new file mode 100755 index 0000000..ba41746 --- /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/snmp.py b/src/op_mode/snmp.py new file mode 100755 index 0000000..e08441f --- /dev/null +++ b/src/op_mode/snmp.py @@ -0,0 +1,77 @@ +#!/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 . +# +# 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 + +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) + os.system('/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 0000000..9d56a95 --- /dev/null +++ b/src/op_mode/snmp_ifmib.py @@ -0,0 +1,128 @@ +#!/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 . +# +# File: snmp_ifmib.py +# Purpose: +# Show SNMP MIB information +# Used by the "run show snmp mib" commands. + +import sys +import argparse +import netifaces +import subprocess + +from vyos.config import Config + +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(i): + proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE) + (out, err) = proc.communicate() + # convert output to string + string = out.decode("utf-8") + + index = 'ifIndex = ' + string.split(':')[0] + return index.replace('\n', '') + +def show_ifalias(i): + proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE) + (out, err) = proc.communicate() + # convert output to string + string = out.decode("utf-8") + + if 'alias' in string: + alias = 'ifAlias = ' + string.split('alias')[1].lstrip() + else: + alias = 'ifAlias = ' + i + + return 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) + proc = subprocess.Popen(['/usr/bin/lspci', '-mm', '-d', device], stdout=subprocess.PIPE) + (out, err) = proc.communicate() + + # convert output to string + string = out.decode("utf-8").split('"') + vendor = string[3] + 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 0000000..92601f1 --- /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 . +# +# 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 0000000..015b2e6 --- /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/version.py b/src/op_mode/version.py new file mode 100755 index 0000000..ce3b3b5 --- /dev/null +++ b/src/op_mode/version.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016 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 . +# +# File: vyos-show-version +# Purpose: +# Displays image version and system information. +# Used by the "run show version" command. + + +import os +import sys +import subprocess +import argparse +import json + +import pystache + +import vyos.version +import vyos.limericks + + +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") + +def read_file(name): + try: + with open (name, "r") as f: + data = f.read() + return data.strip() + except: + # This works since we only read /sys/class/* stuff + # with this function + return "Unknown" + +version_output_tmpl = """ +Version: VyOS {{version}} +Built by: {{built_by}} +Built on: {{built_on}} +Build ID: {{build_id}} + +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_version_data() + + # Get system architecture (well, kernel architecture rather) + version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip() + + + # Get hypervisor name, if any + system_type = "bare metal" + try: + hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip() + system_type = "{0} guest".format(hypervisor) + except subprocess.CalledProcessError: + # hvinfo returns 1 if it cannot detect any hypervisor + pass + version_data['system_type'] = system_type + + + # 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 subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0: + boot_via = "livecd" + elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0: + boot_via = "legacy non-image installation" + version_data['boot_via'] = boot_via + + + # Get hardware details from DMI + version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor') + version_data['hardware_model'] = read_file('/sys/class/dmi/id/product_name') + + # These two assume script is run as root, normal users can't access those files + version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial') + version_data['hardware_uuid'] = read_file('/sys/class/dmi/id/subsystem/id/product_uuid') + + + if args.json: + print(json.dumps(version_data)) + sys.exit(0) + else: + output = pystache.render(version_output_tmpl, version_data).strip() + print(output) + + if args.all: + print("Package versions:") + os.system("dpkg -l") + + if args.funny: + print(vyos.limericks.get_random()) diff --git a/src/tests/helper.py b/src/tests/helper.py new file mode 100644 index 0000000..a7e4f20 --- /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 . +# +# + +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_host_name.py b/src/tests/test_host_name.py new file mode 100644 index 0000000..8c5210d --- /dev/null +++ b/src/tests/test_host_name.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 . +# +# + +import os +import tempfile +import unittest +from unittest import TestCase, mock + +from vyos import ConfigError +try: + from src.conf_mode import host_name +except ModuleNotFoundError: # for unittest.main() + import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + from src.conf_mode import host_name + + +class TestHostName(TestCase): + + def test_get_config(self): + tests = [ + {'name': 'empty_hostname_and_domain', + 'host-name': '', + 'domain-name': '', + 'expected': {"hostname": 'vyos', "domain": '', "fqdn": 'vyos'}}, + {'name': 'empty_hostname', + 'host-name': '', + 'domain-name': 'localdomain', + 'expected': {"hostname": 'vyos', "domain": 'localdomain', "fqdn": 'vyos.localdomain'}}, + {'name': 'has_hostname', + 'host-name': 'router', + 'domain-name': '', + 'expected': {"hostname": 'router', "domain": '', "fqdn": 'router'}}, + {'name': 'has_hostname_and_domain', + 'host-name': 'router', + 'domain-name': 'localdomain', + 'expected': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}}, + ] + for t in tests: + def mocked_return_value(path, default=None): + return t[path.split()[1]] + + with self.subTest(msg=t['name'], hostname=t['host-name'], domain=t['domain-name'], expected=t['expected']): + with mock.patch('vyos.config.Config.return_value', side_effect=mocked_return_value): + actual = host_name.get_config() + self.assertEqual(actual, t['expected']) + + + def test_verify(self): + tests = [ + {'name': 'valid_hostname', + 'config': {"hostname": 'vyos', "domain": 'localdomain', "fqdn": 'vyos.localdomain'}, + 'expected': None}, + {'name': 'invalid_hostname', + 'config': {"hostname": 'vyos..', "domain": '', "fqdn": ''}, + 'expected': ConfigError}, + {'name': 'invalid_hostname_length', + 'config': {"hostname": 'a'*64, "domain": '', "fqdn": ''}, + 'expected': ConfigError} + ] + for t in tests: + with self.subTest(msg=t['name'], config=t['config'], expected=t['expected']): + if t['expected'] is not None: + with self.assertRaises(t['expected']): + host_name.verify(t['config']) + else: + host_name.verify(t['config']) + + def test_generate(self): + tests = [ + {'name': 'has_old_entry', + 'has_old_entry': True, + 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, + 'expected': ['127.0.1.1', 'router.localdomain']}, + {'name': 'no_old_entry', + 'has_old_entry': False, + 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, + 'expected': ['127.0.1.1', 'router.localdomain']}, + ] + for t in tests: + with self.subTest(msg=t['name'], config=t['config'], has_old_entry=t['has_old_entry'], expected=t['expected']): + m = mock.MagicMock(return_value=b'debian') + with mock.patch('subprocess.check_output', m): + host_name.hosts_file = tempfile.mkstemp()[1] + if t['has_old_entry']: + with open(host_name.hosts_file, 'w') as f: + f.writelines(['\n127.0.1.1 {} # VyOS entry'.format('debian')]) + host_name.generate(t['config']) + if len(t['expected']) > 0: + self.assertTrue(os.path.isfile(host_name.hosts_file)) + with open(host_name.hosts_file) as f: + actual = f.read() + self.assertEqual( + t['expected'], actual.splitlines()[1].split()[0:2]) + os.remove(host_name.hosts_file) + else: + self.assertFalse(os.path.isfile(host_name.hosts_file)) + + + def test_apply(self): + tests = [ + {'name': 'valid_hostname', + 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, + 'expected': [mock.call('hostnamectl set-hostname --static router.localdomain'), + mock.call('systemctl restart rsyslog.service')]} + ] + for t in tests: + with self.subTest(msg=t['name'], c=t['config'], expected=t['expected']): + with mock.patch('os.system') as os_system: + host_name.apply(t['config']) + os_system.assert_has_calls(t['expected']) + + +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 0000000..084bd86 --- /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 . +# +# + +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/utils/initial-setup b/src/utils/initial-setup new file mode 100644 index 0000000..37fc457 --- /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-to-commands b/src/utils/vyos-config-to-commands new file mode 100755 index 0000000..8b50f7c --- /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/validators/interface-address b/src/validators/interface-address new file mode 100755 index 0000000..4c20395 --- /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 0000000..51fb72c --- /dev/null +++ b/src/validators/ip-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-single $1 diff --git a/src/validators/ip-host b/src/validators/ip-host new file mode 100755 index 0000000..f2906e8 --- /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 0000000..e58aad3 --- /dev/null +++ b/src/validators/ip-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-net $1 diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address new file mode 100755 index 0000000..872a764 --- /dev/null +++ b/src/validators/ipv4-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-single $1 diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host new file mode 100755 index 0000000..f42feff --- /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 0000000..8ec8a2c --- /dev/null +++ b/src/validators/ipv4-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-net $1 diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address new file mode 100755 index 0000000..e5d68d7 --- /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 0000000..f7a7450 --- /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 0000000..e436163 --- /dev/null +++ b/src/validators/ipv6-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-net $1 diff --git a/src/validators/numeric b/src/validators/numeric new file mode 100755 index 0000000..58a4fac --- /dev/null +++ b/src/validators/numeric @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# numeric value validator +# +# 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 +# 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 . + +import sys +import argparse +import re + +parser = argparse.ArgumentParser() +parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values") +group = parser.add_mutually_exclusive_group() +group.add_argument("-r", "--range", type=str, help="Check if the number is within range (inclusive), example: 1024-65535") +group.add_argument("-n", "--non-negative", action="store_true", help="") +parser.add_argument("number", type=str, help="Number to validate") + +args = parser.parse_args() + +# Try to load the argument +number = None +if args.float: + try: + number = float(args.number) + except: + print("{0} is not a valid floating point number".format(args.number), file=sys.stderr) + sys.exit(1) +else: + try: + number = int(args.number) + except: + print("{0} is not a valid integer number".format(args.number), file=sys.stderr) + sys.exit(1) + +if args.range: + try: + lower, upper = re.match(r'(\d+)\s*\-\s*(\d+)', args.range).groups() + lower, upper = int(lower), int(upper) + except: + print("{0} is not a valid number range",format(args.range), file=sys.stderr) + sys.exit(1) + + if (number < lower) or (number > upper): + print("Number {0} is not in the {1} range".format(number, args.range), file=sys.stderr) + sys.exit(1) +elif args.non_negative: + if number < 0: + print("Number should be non-negative", file=sys.stderr) + sys.exit(1) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..9348520 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +python/ +lxml +pylint +nose +coverage diff --git a/tests/data/interface-definitions/test-op.xml b/tests/data/interface-definitions/test-op.xml new file mode 100644 index 0000000..50bd686 --- /dev/null +++ b/tests/data/interface-definitions/test-op.xml @@ -0,0 +1,21 @@ + + + + + foo + + + + /usr/bin/bar + + bar + + foo bar + interfaces tunnel + + + + + + + diff --git a/tests/data/interface-definitions/test.xml b/tests/data/interface-definitions/test.xml new file mode 100644 index 0000000..fbb302e --- /dev/null +++ b/tests/data/interface-definitions/test.xml @@ -0,0 +1,24 @@ + + + + + foo + + + + + bar + + bar + bar + + + foo bar + interfaces tunnel + + + + + + + -- cgit v1.2.3