summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUnicronNL <kim.sidney@gmail.com>2018-07-01 22:48:06 +0200
committerUnicronNL <kim.sidney@gmail.com>2018-07-01 22:48:06 +0200
commitaaff68c0d55dc32f8972b9bde3239bb0923de140 (patch)
tree3d1d5aa18bd8d100e32d6ee2a4012f49f990117b
parentcf4738ddbff6858975ff5f2bb0d656453e563848 (diff)
downloadvyos-salt-minion-aaff68c0d55dc32f8972b9bde3239bb0923de140.tar.gz
vyos-salt-minion-aaff68c0d55dc32f8972b9bde3239bb0923de140.zip
add vyos 1.x
-rw-r--r--.gitignore117
-rw-r--r--LICENSE.GPL339
-rw-r--r--LICENSE.LGPL502
-rw-r--r--Makefile53
-rw-r--r--README.md56
-rw-r--r--data/interface-types.json17
-rw-r--r--debian/changelog40
-rw-r--r--debian/compat1
-rw-r--r--debian/control39
-rw-r--r--debian/copyright35
-rw-r--r--debian/lintian-overrides6
-rwxr-xr-xdebian/rules61
-rw-r--r--interface-definitions/bcast-relay.xml68
-rw-r--r--interface-definitions/beep-on-boot.xml21
-rw-r--r--interface-definitions/cron.xml75
-rw-r--r--interface-definitions/dns-forwarding.xml135
-rw-r--r--interface-definitions/host-name.xml26
-rw-r--r--interface-definitions/mdns-repeater.xml32
-rw-r--r--interface-definitions/ntp.xml89
-rw-r--r--interface-definitions/snmp.xml606
-rw-r--r--interface-definitions/ssh.xml157
-rw-r--r--op-mode-definitions/bandwidth-monitor.xml23
-rw-r--r--op-mode-definitions/dns-forwarding.xml72
-rw-r--r--op-mode-definitions/poweroff.xml40
-rw-r--r--op-mode-definitions/reboot.xml40
-rw-r--r--op-mode-definitions/show-arp.xml25
-rw-r--r--op-mode-definitions/show-bridge.xml37
-rw-r--r--op-mode-definitions/show-configuration.xml39
-rw-r--r--op-mode-definitions/show-date.xml30
-rw-r--r--op-mode-definitions/show-disk.xml24
-rw-r--r--op-mode-definitions/show-hardware.xml95
-rw-r--r--op-mode-definitions/show-raid.xml18
-rw-r--r--op-mode-definitions/show-users.xml30
-rw-r--r--op-mode-definitions/snmp.xml111
-rw-r--r--op-mode-definitions/traffic-dump.xml45
-rw-r--r--op-mode-definitions/version.xml27
-rw-r--r--python/setup.py21
-rw-r--r--python/vyos/__init__.py1
-rw-r--r--python/vyos/base.py18
-rw-r--r--python/vyos/config.py416
-rw-r--r--python/vyos/configtree.py261
-rw-r--r--python/vyos/defaults.py19
-rw-r--r--python/vyos/interfaces.py45
-rw-r--r--python/vyos/limericks.py64
-rw-r--r--python/vyos/util.py65
-rw-r--r--python/vyos/version.py68
-rw-r--r--schema/interface_definition.rnc154
-rw-r--r--schema/interface_definition.rng275
-rw-r--r--schema/op-mode-definition.rnc107
-rw-r--r--schema/op-mode-definition.rng165
-rwxr-xr-xscripts/build-command-op-templates225
-rwxr-xr-xscripts/build-command-templates295
-rw-r--r--sonar-project.properties21
-rw-r--r--sphinx/Makefile177
-rw-r--r--sphinx/source/conf.py261
-rw-r--r--sphinx/source/index.rst22
-rwxr-xr-xsrc/completion/list_disks.sh5
-rwxr-xr-xsrc/completion/list_dumpable_interfaces.py14
-rwxr-xr-xsrc/completion/list_interfaces.py31
-rwxr-xr-xsrc/completion/list_raidset.sh3
-rwxr-xr-xsrc/conf_mode/bcast_relay.py122
-rwxr-xr-xsrc/conf_mode/beep_if_fully_booted.py42
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py234
-rwxr-xr-xsrc/conf_mode/host_name.py117
-rw-r--r--src/conf_mode/lldp.py217
-rwxr-xr-xsrc/conf_mode/mdns_repeater.py93
-rwxr-xr-xsrc/conf_mode/ntp.py173
-rwxr-xr-xsrc/conf_mode/snmp.py804
-rwxr-xr-xsrc/conf_mode/ssh.py255
-rwxr-xr-xsrc/conf_mode/task_scheduler.py148
-rwxr-xr-xsrc/helpers/commands-pipe.py29
-rwxr-xr-xsrc/helpers/validate-value.py43
-rwxr-xr-xsrc/migration-scripts/config-management/0-to-131
-rwxr-xr-xsrc/migration-scripts/system/7-to-845
-rwxr-xr-xsrc/op_mode/cpu_summary.py24
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py49
-rwxr-xr-xsrc/op_mode/dns_forwarding_restart.sh8
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py33
-rwxr-xr-xsrc/op_mode/maya_date.py209
-rwxr-xr-xsrc/op_mode/show-configuration-files.sh10
-rwxr-xr-xsrc/op_mode/show-disk-format.sh8
-rwxr-xr-xsrc/op_mode/show-raid.sh17
-rwxr-xr-xsrc/op_mode/snmp.py77
-rwxr-xr-xsrc/op_mode/snmp_ifmib.py128
-rwxr-xr-xsrc/op_mode/snmp_v3.py180
-rwxr-xr-xsrc/op_mode/snmp_v3_showcerts.sh8
-rwxr-xr-xsrc/op_mode/version.py123
-rw-r--r--src/tests/helper.py27
-rw-r--r--src/tests/test_host_name.py130
-rw-r--r--src/tests/test_task_scheduler.py130
-rw-r--r--src/utils/initial-setup40
-rwxr-xr-xsrc/utils/vyos-config-to-commands29
-rwxr-xr-xsrc/validators/interface-address3
-rwxr-xr-xsrc/validators/ip-address3
-rwxr-xr-xsrc/validators/ip-host3
-rwxr-xr-xsrc/validators/ip-prefix3
-rwxr-xr-xsrc/validators/ipv4-address3
-rwxr-xr-xsrc/validators/ipv4-host3
-rwxr-xr-xsrc/validators/ipv4-prefix3
-rwxr-xr-xsrc/validators/ipv6-address3
-rwxr-xr-xsrc/validators/ipv6-host3
-rwxr-xr-xsrc/validators/ipv6-prefix3
-rwxr-xr-xsrc/validators/numeric62
-rw-r--r--test-requirements.txt5
-rw-r--r--tests/data/interface-definitions/test-op.xml21
-rw-r--r--tests/data/interface-definitions/test.xml24
106 files changed, 9589 insertions, 0 deletions
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., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/LICENSE.LGPL b/LICENSE.LGPL
new file mode 100644
index 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.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
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 <christian@poessinger.com> 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 <christian@poessinger.com> 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 <christian@poessinger.com> Sat, 06 Jan 2018 13:18:30 +0100
+
+vyos-1x (1.0.2) unstable; urgency=low
+
+ * Added mdns-repeater configuration nodes
+
+ -- Christian Poessinger <christian@poessinger.com> 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 <daniil@baturin.org> Thu, 17 Aug 2017 22:22:17 -0400
+
+vyos-1x (1.0.0) unstable; urgency=low
+
+ * Created the package
+
+ -- Daniil Baturin <daniil@baturin.org> 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 <maintainers@vyos.net>
+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 <daniil@baturin.org> on
+Thu, 17 Aug 2017 20:17:04 -0400
+
+It's original content from the GIT repository <http://github.com/vyos/vyos-1x>
+
+Upstream Author:
+
+ <maintainers@vyos.net>
+
+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 <daniil@baturin.org> and
+is licensed under the GPL, see above.
diff --git a/debian/lintian-overrides b/debian/lintian-overrides
new file mode 100644
index 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 @@
+<?xml version="1.0"?>
+<!-- UDP broadcast relay configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="broadcast-relay">
+ <properties>
+ <help>UDP Broadcast Relay parameters</help>
+ </properties>
+ <children>
+ <tagNode name="id" owner="${vyos_conf_scripts_dir}/bcast_relay.py">
+ <properties>
+ <help>Unique ID for each UDP port to forward</help>
+ <valueHelp>
+ <format>1-99</format>
+ <description>Numerical ID #</description>
+ </valueHelp>
+ <priority>990</priority>
+ <constraint>
+ <validator name="numeric" argument="--range 1-99"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>Set source IP of forwarded packets, otherwise original senders address is used</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Optional source address for forwarded packets</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to repeat UDP broadcasts to [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination or source port to listen and retransmit on [REQUIRED]</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>UDP port to listen on</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/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 @@
+<?xml version="1.0"?>
+
+<!-- beep once the login target has been reached -->
+
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="options">
+ <children>
+ <leafNode name="beep-if-fully-booted" owner="${vyos_conf_scripts_dir}/beep_if_fully_booted.py">
+ <properties>
+ <help>plays sound via system speaker when you can login</help>
+ <valueless/>
+ <priority>9999</priority>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+
+<!-- Cron configuration -->
+
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="task-scheduler">
+ <properties>
+ <help>Task scheduler settings</help>
+ </properties>
+ <children>
+ <tagNode name="task" owner="${vyos_conf_scripts_dir}/task_scheduler.py">
+ <properties>
+ <help>Scheduled task</help>
+ <valueHelp>
+ <format>&lt;string&gt;</format>
+ <description>Task name</description>
+ </valueHelp>
+ <priority>999</priority>
+ </properties>
+ <children>
+ <leafNode name="crontab-spec">
+ <properties>
+ <help>UNIX crontab time specification string</help>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Execution interval</help>
+ <valueHelp>
+ <format>&lt;minutes&gt;</format>
+ <description>Execution interval in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;minutes&gt;m</format>
+ <description>Execution interval in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;hours&gt;h</format>
+ <description>Execution interval in hours</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;days&gt;d</format>
+ <description>Execution interval in days</description>
+ </valueHelp>
+ <constraint>
+ <regex>[1-9]([0-9]*)([mhd]{0,1})</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="executable">
+ <properties>
+ <help>Executable path and arguments</help>
+ </properties>
+ <children>
+ <leafNode name="path">
+ <properties>
+ <help>Path to executable</help>
+ </properties>
+ </leafNode>
+ <leafNode name="arguments">
+ <properties>
+ <help>Arguments passed to the executable</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/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 @@
+<?xml version="1.0"?>
+<!-- DNS forwarder configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dns">
+ <children>
+ <node name="forwarding" owner="${vyos_conf_scripts_dir}/dns_forwarding.py">
+ <properties>
+ <help>DNS forwarding</help>
+ <priority>918</priority>
+ </properties>
+ <children>
+ <leafNode name="cache-size">
+ <properties>
+ <help>DNS forwarding cache size</help>
+ <valueHelp>
+ <format>0-10000</format>
+ <description>DNS forwarding cache size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-10000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dhcp">
+ <properties>
+ <help>Use DNS servers received from DHCP server for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <tagNode name="domain">
+ <properties>
+ <help>Domain to forward to a custom DNS server</help>
+ </properties>
+ <children>
+ <leafNode name="server">
+ <properties>
+ <help>Domain Name Server (DNS) to forward queries to</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="ignore-hosts-file">
+ <properties>
+ <help>Do not use local /etc/hosts file in name resolution</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses to listen for DNS queries [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-on">
+ <properties>
+ <help>Interface to listen for DNS queries [DEPRECATED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="negative-ttl">
+ <properties>
+ <help>Maximum amount of time negative entries are cached</help>
+ <valueHelp>
+ <format>0-7200</format>
+ <description>Seconds to cache NXDOMAIN entries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-7200"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Servers (DNS) addresses</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="system">
+ <properties>
+ <help>Use system name servers</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/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 @@
+<?xml version="1.0"?>
+
+<!-- host-name configuration -->
+
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>System host name (default: vyos)</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>System domain name</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<!-- mDNS repeater configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="mdns">
+ <properties>
+ <help>Multicast DNS (mDNS) parameters</help>
+ </properties>
+ <children>
+ <node name="repeater" owner="${vyos_conf_scripts_dir}/mdns_repeater.py">
+ <properties>
+ <help>mDNS repeater configuration</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to repeat mdns advertisements to [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/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 @@
+<?xml version="1.0"?>
+<!-- NTP configuration -->
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="ntp" owner="${vyos_conf_scripts_dir}/ntp.py">
+ <properties>
+ <help>Network Time Protocol (NTP) configuration</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="server">
+ <properties>
+ <help>Network Time Protocol (NTP) server</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic">
+ <properties>
+ <help>Allow server to be configured even if not reachable</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="noselect">
+ <properties>
+ <help>Marks the server as unused</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="preempt">
+ <properties>
+ <help>Specifies the association as preemptable rather than the default persistent</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="prefer">
+ <properties>
+ <help>Marks the server as preferred</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="allow-clients">
+ <properties>
+ <help>Network Time Protocol (NTP) server options</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses to listen for NTP queries</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Network Time Protocol (NTP) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Network Time Protocol (NTP) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<!-- SNMP forwarder configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="snmp" owner="${vyos_conf_scripts_dir}/snmp.py">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP)</help>
+ <priority>980</priority>
+ </properties>
+ <children>
+ <tagNode name="community">
+ <properties>
+ <help>Community name [REQUIRED]</help>
+ <constraint>
+ <regex>^[a-zA-Z0-9\-_]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="authorization">
+ <properties>
+ <help>Authorization type (default: 'ro')</help>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ro|rw)</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="client">
+ <properties>
+ <help>IP address of SNMP client allowed to contact system</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="network">
+ <properties>
+ <help>Subnet of SNMP client(s) allowed to contact system</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="contact">
+ <properties>
+ <help>Contact information</help>
+ <constraint>
+ <regex>.{1,255}</regex>
+ </constraint>
+ <constraintErrorMessage>Contact information is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description information</help>
+ <constraint>
+ <regex>.{1,255}</regex>
+ </constraint>
+ <constraintErrorMessage>Description is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="listen-address">
+ <properties>
+ <help>IP address to listen for incoming SNMP requests</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to listen for incoming SNMP requests</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming SNMP requests</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>Port for SNMP service (default: '161')</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="location">
+ <properties>
+ <help>Location information</help>
+ <constraint>
+ <regex>.{1,255}</regex>
+ </constraint>
+ <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="smux-peer">
+ <properties>
+ <help>Register a subtree for SMUX-based processing</help>
+ <valueHelp>
+ <format>oid</format>
+ <description>Object Identifier</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="trap-source">
+ <properties>
+ <help>SNMP trap source address</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="trap-target">
+ <properties>
+ <help>Address of trap target</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="community">
+ <properties>
+ <help>Community used when sending trap information</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination port used for trap notification</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="v3">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP) v3</help>
+ </properties>
+ <children>
+ <leafNode name="engineid">
+ <properties>
+ <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help>
+ <constraint>
+ <regex>^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex>
+ </constraint>
+ <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="group">
+ <properties>
+ <help>Specifies the group with name groupname</help>
+ </properties>
+ <children>
+ <leafNode name="mode">
+ <properties>
+ <help>Define group access permission (default: 'ro')</help>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ro|rw)</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="seclevel">
+ <properties>
+ <help>Security levels</help>
+ <valueHelp>
+ <format>noauth</format>
+ <description>Messages not authenticated and not encrypted (noAuthNoPriv)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Messages are authenticated but not encrypted (authNoPriv)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>priv</format>
+ <description>Messages are authenticated and encrypted (authPriv)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(noauth|auth|priv)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="view">
+ <properties>
+ <help>Defines the name of view</help>
+ <completionHelp>
+ <path>service snmp v3 view</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="trap-target">
+ <properties>
+ <help>Defines SNMP target for inform or traps for IP</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of trap target</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of trap target</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="auth">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-key">
+ <properties>
+ <help>Defines the encrypted key for authentication</help>
+ <constraint>
+ <regex>^0x[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-key">
+ <properties>
+ <help>Defines the clear text key for authentication</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol used for authentication (default: 'md5')</help>
+ <valueHelp>
+ <format>md5</format>
+ <description>Message Digest 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha</format>
+ <description>Secure Hash Algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="engineid">
+ <properties>
+ <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help>
+ <constraint>
+ <regex>^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex>
+ </constraint>
+ <constraintErrorMessage>ID must contain from 2 to 36 hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="privacy">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-key">
+ <properties>
+ <help>Defines the encrypted key for privacy protocol</help>
+ <constraint>
+ <regex>^0x[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-key">
+ <properties>
+ <help>Defines the clear text key for privacy protocol</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol for privacy (default: 'des')</help>
+ <valueHelp>
+ <format>des</format>
+ <description>Data Encryption Standard</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes</format>
+ <description>Advanced Encryption Standard</description>
+ </valueHelp>
+ <constraint>
+ <regex>(des|aes)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="protocol">
+ <properties>
+ <help>Defines protocol for notification between TCP and UDP</help>
+ <valueHelp>
+ <format>tcp</format>
+ <description>Use Transmission Control Protocol for notifications</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>Use User Datagram Protocol for notifications</description>
+ </valueHelp>
+ <constraint>
+ <regex>(tcp|udp)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Specifies the type of notification between inform and trap (default: 'inform')</help>
+ <valueHelp>
+ <format>inform</format>
+ <description>Use INFORM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>trap</format>
+ <description>Use TRAP</description>
+ </valueHelp>
+ <constraint>
+ <regex>(inform|trap)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Defines username for authentication</help>
+ <completionHelp>
+ <path>service snmp v3 user</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="tsm">
+ <properties>
+ <help>Specifies that SNMPv3 uses the Transport Security Model (TSM)</help>
+ </properties>
+ <children>
+ <leafNode name="local-key">
+ <properties>
+ <help>Fingerprint of a TSM server certificate</help>
+ <constraint>
+ <regex>^[0-9A-F]{2}(:[0-9A-F]{2}){19}$</regex>
+ </constraint>
+ <constraintErrorMessage>Value can be finger print key or filename in /config/snmp/tls/certs</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Defines the port used for TSM (default: '10161')</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="user">
+ <properties>
+ <help>Specifies the user with name username</help>
+ <constraint>
+ <regex>^[^\(\)\|\-]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Illegal characters in name</constraintErrorMessage>
+ </properties>
+ <children>
+ <node name="auth">
+ <properties>
+ <help>Specifies the auth</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-key">
+ <properties>
+ <help>Defines the encrypted key for authentication</help>
+ <constraint>
+ <regex>^0x[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-key">
+ <properties>
+ <help>Defines the clear text key for authentication</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol used for authentication (default: 'md5')</help>
+ <valueHelp>
+ <format>md5</format>
+ <description>Message Digest 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha</format>
+ <description>Secure Hash Algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="engineid">
+ <properties>
+ <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help>
+ <constraint>
+ <regex>^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex>
+ </constraint>
+ <constraintErrorMessage>ID must contain from 2 to 36 hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="group">
+ <properties>
+ <help>Specifies group for user name</help>
+ <completionHelp>
+ <path>service snmp v3 group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>Define users access permission (default: 'ro')</help>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ro|rw)</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="privacy">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-key">
+ <properties>
+ <help>Defines the encrypted key for privacy protocol</help>
+ <constraint>
+ <regex>^0x[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must start from '0x' and contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-key">
+ <properties>
+ <help>Defines the clear text key for privacy protocol</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol for privacy (default: 'des')</help>
+ <valueHelp>
+ <format>des</format>
+ <description>Data Encryption Standard</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes</format>
+ <description>Advanced Encryption Standard</description>
+ </valueHelp>
+ <constraint>
+ <regex>(des|aes)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="tsm-key">
+ <properties>
+ <help>Specifies finger print or file name of TSM certificate</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="view">
+ <properties>
+ <help>Specifies the view with name viewname</help>
+ <constraint>
+ <regex>^[^\(\)\|\-]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Illegal characters in name</constraintErrorMessage>
+ </properties>
+ <children>
+ <tagNode name="oid">
+ <properties>
+ <help>Specifies the oid</help>
+ <constraint>
+ <regex>^[0-9]+(\\.[0-9]+)*$</regex>
+ </constraint>
+ <constraintErrorMessage>OID must start from a number</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="exclude">
+ <properties>
+ <help>Exclude is an optional argument</help>
+ </properties>
+ </leafNode>
+ <leafNode name="mask">
+ <properties>
+ <help>Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant</help>
+ <constraint>
+ <regex>^[0-9a-f]{2}([\\.:][0-9a-f]{2})*$</regex>
+ </constraint>
+ <constraintErrorMessage>MASK is a list of hex octets, separated by '.' or ':'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+
+<!--SSH configuration -->
+
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="ssh" owner="${vyos_conf_scripts_dir}/ssh.py">
+ <properties>
+ <help>Secure SHell (SSH) protocol</help>
+ <priority>500</priority>
+ </properties>
+ <children>
+ <node name="access-control">
+ <properties>
+ <help>SSH user/group access controls. Directives are processed in this order: deny-users, allow-users, deny-groups and allow-groups</help>
+ </properties>
+ <children>
+ <node name="allow">
+ <children>
+ <leafNode name="group">
+ <properties>
+ <help>Allow members of a group to login</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Allow specific users to login</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="deny">
+ <children>
+ <leafNode name="group">
+ <properties>
+ <help>Disallow members of a group to login</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Disallow specific users to login</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="allow-root">
+ <properties>
+ <help>Allow the root user to login</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ciphers">
+ <properties>
+ <help>Allowed ciphers</help>
+ <completionHelp>
+ <script>ssh -Q cipher | tr '\n' ' '</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-host-validation">
+ <properties>
+ <help>Don't validate the remote host name with DNS</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-password-authentication">
+ <properties>
+ <help>Disable password-based authentication</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="key-exchange">
+ <properties>
+ <help>Allowed key exchange (KEX) algorithms</help>
+ <completionHelp>
+ <script>ssh -Q kex | tr '\n' ' '</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Local addresses SSH service should listen on</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="loglevel">
+ <properties>
+ <help>Log level</help>
+ <valueHelp>
+ <format>QUIET</format>
+ <description>stay silent</description>
+ </valueHelp>
+ <valueHelp>
+ <format>FATAL</format>
+ <description>log fatals only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ERROR</format>
+ <description>log errors and fatals only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>INFO</format>
+ <description>default log level</description>
+ </valueHelp>
+ <valueHelp>
+ <format>VERBOSE</format>
+ <description>enable logging of failed login attempts</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="mac">
+ <properties>
+ <help>Allowed message authentication code (MAC) algorithms</help>
+ <completionHelp>
+ <script>ssh -Q mac | tr '\n' ' '</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for SSH service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="bandwidth">
+ <properties>
+ <help>Monitor interface bandwidth in real time</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <command>bmon -p $4</command>
+ <properties>
+ <help>Monitor bandwidth usage on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="dns">
+ <children>
+ <node name="forwarding">
+ <properties>
+ <help>Show DNS forwarding information</help>
+ </properties>
+ <children>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show DNS forwarding statistics</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Restart a DNS service</help>
+ </properties>
+ <children>
+ <leafNode name="forwarding">
+ <properties>
+ <help>Restart DNS forwarding service</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="reset">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Reset a DNS service state</help>
+ </properties>
+ <children>
+ <node name="forwarding">
+ <properties>
+ <help>Reset DNS forwarding cache</help>
+ </properties>
+ <children>
+ <tagNode name="domain">
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py $5</command>
+ <properties>
+ <help>Reset DNS forwarding cache for a domain</help>
+ </properties>
+ </tagNode>
+ <leafNode name="all">
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py --all</command>
+ <properties>
+ <help>Reset DNS forwarding cache</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="poweroff">
+ <properties>
+ <help>Poweroff the system</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff</command>
+ <children>
+ <leafNode name="now">
+ <properties>
+ <help>Poweroff the system without confirmation</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff --now</command>
+ </leafNode>
+
+ <leafNode name="cancel">
+ <properties>
+ <help>Cancel a pending poweroff</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff_cancel</command>
+ </leafNode>
+
+
+ <tagNode name="at">
+ <properties>
+ <help>Poweroff at a specific time</help>
+ <completionHelp>
+ <list>HH:MM</list>
+ <list>MMDDYY</list>
+ <list>midnight</list>
+ <list>noon</list>
+ </completionHelp>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-poweroff.pl --action poweroff_at --at_time '$3'</command>
+
+ </tagNode>
+
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reboot">
+ <properties>
+ <help>Reboot the system</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot</command>
+ <children>
+ <leafNode name="now">
+ <properties>
+ <help>Reboot the system without confirmation</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot --now</command>
+ </leafNode>
+
+ <leafNode name="cancel">
+ <properties>
+ <help>Cancel a pending reboot</help>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot_cancel</command>
+ </leafNode>
+
+
+ <tagNode name="at">
+ <properties>
+ <help>Reboot at a specific time</help>
+ <completionHelp>
+ <list>HH:MM</list>
+ <list>MMDDYY</list>
+ <list>midnight</list>
+ <list>noon</list>
+ </completionHelp>
+ </properties>
+ <command>/opt/vyatta/bin/sudo-users/vyatta-reboot.pl --action reboot_at --at_time '$3'</command>
+
+ </tagNode>
+
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Show Address Resolution Protocol (ARP) information</help>
+ </properties>
+ <command>/usr/sbin/arp -e -n</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show Address Resolution Protocol (ARP) cache for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py -b</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/arp -e -n -i '$4'</command>
+ </tagNode>
+ </children>
+ </node>
+
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-bridge.xml b/op-mode-definitions/show-bridge.xml
new file mode 100644
index 0000000..a324195
--- /dev/null
+++ b/op-mode-definitions/show-bridge.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="bridge">
+ <properties>
+ <help>Show bridging information</help>
+ </properties>
+ <command>/sbin/brctl show</command>
+ </leafNode>
+ <tagNode name="bridge">
+ <properties>
+ <help>Show bridge information for a given bridge interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script>
+ </completionHelp>
+ </properties>
+ <command>/sbin/brctl show '$3'</command>
+ <children>
+ <leafNode name="macs">
+ <properties>
+ <help>Show bridge Media Access Control (MAC) address table</help>
+ </properties>
+ <command>/sbin/brctl showmacs '$3'</command>
+ </leafNode>
+ <leafNode name="spanning-tree">
+ <properties>
+ <help>Show bridge spanning tree information</help>
+ </properties>
+ <command>/sbin/brctl showstp '$3'</command>
+ </leafNode>
+ </children>
+ </tagNode>
+
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-configuration.xml b/op-mode-definitions/show-configuration.xml
new file mode 100644
index 0000000..0b1507b
--- /dev/null
+++ b/op-mode-definitions/show-configuration.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="configuration">
+ <properties>
+ <help>Show available saved configurations</help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-active-only --show-hide-secrets</command>
+
+ <children>
+ <node name="all">
+ <properties>
+ <help>Show running configuration (including default values)</help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets</command>
+ </node>
+ <node name="commands">
+ <properties>
+ <help> Show running configuration as set commands </help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command>
+ </node>
+ <node name="files">
+ <properties>
+ <help> Show available saved configurations </help>
+ </properties>
+ <!-- no admin check -->
+ <command>${vyos_op_scripts_dir}/show-configuration-files.sh</command>
+ </node>
+ </children>
+ </node>
+
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="date">
+ <properties>
+ <help>Show system time and date</help>
+ </properties>
+ <command>/bin/date</command>
+ <children>
+ <node name="utc">
+ <properties>
+ <help>Show system date and time as Coordinated Universal Time</help>
+ </properties>
+ <command>/bin/date -u</command>
+ <children>
+ <leafNode name="maya">
+ <properties>
+ <help>Show UTC date in Maya calendar format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/maya_date.py $(date +%s)</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+ </children>
+ </node>
+</interfaceDefinition>
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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <tagNode name="disk">
+ <properties>
+ <help>Show status of disk device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.sh</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="format">
+ <properties>
+ <help>Show disk drive formatting</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show-disk-format.sh $3</command>
+ </leafNode>
+ </children>
+ </tagNode>
+
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="hardware">
+ <properties>
+ <help>Show system hardware details</help>
+ </properties>
+ <children>
+ <node name="cpu">
+ <properties>
+ <help>Show CPU info</help>
+ </properties>
+ <command>lscpu</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help> Show system CPU details</help>
+ </properties>
+ <command>cat /proc/cpuinfo</command>
+ </node>
+ <node name="summary">
+ <properties>
+ <help>Show CPU's on system</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/cpu_summary.py</command>
+ </node>
+ </children>
+ </node>
+
+ <node name="dmi">
+ <properties>
+ <help>Show system DMI details</help>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-dmi</command>
+ </node>
+
+ <node name="mem">
+ <properties>
+ <help>Show system RAM details</help>
+ </properties>
+ <command>cat /proc/meminfo</command>
+ </node>
+
+ <node name="pci">
+ <properties>
+ <help>Show system PCI bus details</help>
+ </properties>
+ <command>lspci</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show verbose system PCI bus details</help>
+ </properties>
+ <command>lspci -vvv</command>
+ </node>
+ </children>
+ </node>
+
+
+ <node name="scsi">
+ <properties>
+ <help>Show SCSI device information</help>
+ </properties>
+ <command>lsscsi</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show detailed SCSI device information</help>
+ </properties>
+ <command>lsscsi -vvv</command>
+ </node>
+ </children>
+ </node>
+
+ <node name="usb">
+ <properties>
+ <help>Show peripherals connected to the USB bus</help>
+ </properties>
+ <command>lsusb</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show detailed USB bus information</help>
+ </properties>
+ <command>lsusb -v</command>
+ </node>
+ </children>
+ </node>
+
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml
new file mode 100644
index 0000000..b093074
--- /dev/null
+++ b/op-mode-definitions/show-raid.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <tagNode name="raid">
+ <properties>
+ <help>Show statis of RAID set</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_raidset.sh</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_raid.sh $3</command>
+
+ </tagNode>
+
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="users">
+ <properties>
+ <help>Show user information</help>
+ </properties>
+ <command>who -H</command>
+ <children>
+ <node name="recent">
+ <properties>
+ <help>Show 10 recently logged in users</help>
+ </properties>
+ <command>last -aF -n 10 | sed -e 's/^wtmp begins/Displaying logins since/'</command>
+ </node>
+ <tagNode name="recent">
+ <properties>
+ <help>Show specified number of recently logged in users</help>
+ <completionHelp>
+ <list>NUMBER</list>
+ </completionHelp>
+ </properties>
+ <command>last -aF -n $4 | sed -e 's/^wtmp begins/Displaying logins since/'</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="snmp">
+ <properties>
+ <help>Show status of SNMP on localhost</help>
+ </properties>
+ <children>
+ <tagNode name="community">
+ <properties>
+ <help>Show status of SNMP community</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/snmp.py --allowed</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp.py --community="$4"</command>
+ <children>
+ <tagNode name="host">
+ <properties>
+ <help>Show status of SNMP on remote host</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp.py --community="$4" --host "$6"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="mib">
+ <properties>
+ <help>Show SNMP MIB information</help>
+ </properties>
+ <children>
+ <node name="ifmib">
+ <properties>
+ <help>Show all SNMP interfaces MIB information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py</command>
+ <children>
+ <tagNode name="ifAlias">
+ <properties>
+ <help>Show SNMP ifAlias for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifalias="$6"</command>
+ </tagNode>
+ <tagNode name="ifDescr">
+ <properties>
+ <help>Show SNMP ifDescr for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifdescr="$6"</command>
+ </tagNode>
+ <tagNode name="ifIndex">
+ <properties>
+ <help>Show SNMP ifDescr for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifindex="$6"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="v3">
+ <properties>
+ <help>Show SNMP v3 status on localhost</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --all</command>
+ <children>
+ <leafNode name="certificates">
+ <properties>
+ <help>Show TSM certificates</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3_showcerts.sh</command>
+ </leafNode>
+ <leafNode name="group">
+ <properties>
+ <help>Show the list of configured groups</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --group</command>
+ </leafNode>
+ <leafNode name="trap-target">
+ <properties>
+ <help>Show the list of configured targets</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --trap</command>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Show the list of configured users</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --user</command>
+ </leafNode>
+ <leafNode name="view">
+ <properties>
+ <help>Show the list of configured views</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --view</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="traffic">
+ <properties>
+ <help>Monitor traffic dumps</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <command>tcpdump -i $4</command>
+ <properties>
+ <help>Monitor traffic dump from an interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_dumpable_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="filter">
+ <command>tcpdump -n -i $4 $6</command>
+ <properties>
+ <help>Monitor traffic matching filter conditions</help>
+ </properties>
+ </tagNode>
+ <tagNode name="save">
+ <command>tcpdump -n -i $4 -w $6</command>
+ <properties>
+ <help>Save traffic dump from an interface to a file</help>
+ </properties>
+ <children>
+ <tagNode name="filter">
+ <command>tcpdump -n -i $4 -w $6 $8</command>
+ <properties>
+ <help>Save a dump of traffic matching filter conditions to a file</help>
+ </properties>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="version">
+ <properties>
+ <help>Show system version information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/version.py</command>
+ <children>
+ <leafNode name="funny">
+ <properties>
+ <help>Show system version and some fun stuff</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/version.py --funny</command>
+ </leafNode>
+ <leafNode name="all">
+ <properties>
+ <help>Show system version and versions of all packages</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/version.py --all</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+class ConfigError(Exception):
+ pass
diff --git a/python/vyos/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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+A library for reading VyOS running config data.
+
+This library is used internally by all config scripts of VyOS,
+but its API should be considered stable and 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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+directories = {
+ "data": "/usr/share/vyos/"
+}
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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import 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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import random
+
+limericks = [
+
+"""
+A programmer who's name was Searle
+Once wrote a long program in Perl.
+Despite very few quirks
+No one got how it works,
+Not even the interpreter.
+""",
+
+"""
+There was a young lady of Maine
+Who set up IPsec VPN.
+Problems didn't arise
+'til other vendors' device
+had to add she to that VPN.
+""",
+
+"""
+One day a programmer from York
+started his own Vyatta fork.
+Though he was a huge geek,
+it still took him a week
+to get the damn build scripts to work.
+""",
+
+"""
+A network admin from Hong Kong
+knew MPPE cipher's not strong.
+But he was behind NAT,
+so he put up we that,
+sad network admin from Hong Kong.
+""",
+
+"""
+A network admin named Drake
+greeted friends with a three-way handshake
+and refused to proceed
+if they didn't complete it,
+that standards-compliant guy Drake.
+"""
+
+]
+
+
+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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+
+
+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 <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+VyOS version data access library.
+
+VyOS stores its version data, which include the version number and some
+additional information in a JSON file. This module provides a convenient
+interface to reading it.
+
+Example of the version data dict::
+ {
+ 'built_by': 'autobuild@vyos.net',
+ 'build_id': '021ac2ee-cd07-448b-9991-9c68d878cddd',
+ 'version': '1.2.0-rolling+201806200337',
+ 'built_on': 'Wed 20 Jun 2018 03:37 UTC'
+ }
+"""
+
+import os
+import json
+
+import vyos.defaults
+
+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 <maintainers@vyos.net>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+# The language of this file is compact form RELAX-NG
+# http://relaxng.org/compact-tutorial-20030326.htm
+# (unless converted to XML, then just RELAX-NG :)
+
+# Interface definition starts with interfaceDefinition tag that may contain node tags
+start = element interfaceDefinition
+{
+ node*
+}
+
+# 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. <list>foo bar baz</list>),
+# as a configuration path (e.g. <path>interfaces ethernet</path>),
+# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+completionHelp = element completionHelp
+{
+ (element list { text })* &
+ (element path { text })* &
+ (element script { text })*
+}
diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng
new file mode 100644
index 0000000..195ef27
--- /dev/null
+++ b/schema/interface_definition.rng
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+ <!--
+ interface_definition.rnc: VyConf reference tree XML grammar
+
+ Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ USA
+ -->
+ <!--
+ The language of this file is compact form RELAX-NG
+ http://relaxng.org/compact-tutorial-20030326.htm
+ (unless converted to XML, then just RELAX-NG :)
+ -->
+ <!-- Interface definition starts with interfaceDefinition tag that may contain node tags -->
+ <start>
+ <element name="interfaceDefinition">
+ <zeroOrMore>
+ <ref name="node"/>
+ </zeroOrMore>
+ </element>
+ </start>
+ <!--
+ node tag may contain node, leafNode, or tagNode tags
+ Those are intermediate configuration nodes that may only contain
+ other nodes and must not have values
+ -->
+ <define name="node">
+ <element name="node">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Tag nodes are containers for nodes without predefined names, like network interfaces
+ or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+ Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+ They must not contain other tag nodes
+ -->
+ <define name="tagNode">
+ <element name="tagNode">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <ref name="children"/>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Leaf nodes are terminal configuration nodes that can't have children,
+ but can have values.
+ Leaf node may contain one or more valueConstraint tags
+ If multiple valueConstraint tags are used, they work a logical OR
+ Leaf nodes can have "multi" attribute that indicated that it can have
+ more than one value
+ -->
+ <define name="leafNode">
+ <element name="leafNode">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <ref name="properties"/>
+ </element>
+ </define>
+ <!-- Normal and tag nodes may have children -->
+ <define name="children">
+ <element name="children">
+ <oneOrMore>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ <ref name="leafNode"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!--
+ Nodes may have properties
+ For simplicity, any property is allowed in any node,
+ but whether they are used or not is implementation-defined
+
+ Leaf nodes may differ in number of values that can be
+ associated with them.
+ By default, a leaf node can have only one value.
+ "multi" tag means a node can have one or more values,
+ "valueless" means it can have no values at all.
+ "hidden" means node visibility can be toggled, eg 'dangerous' commands,
+ "secret" allows a node to hide its value from unprivileged users.
+
+ "priority" is used to influence node processing order for nodes
+ with exact same dependencies and in compatibility modes.
+ -->
+ <define name="properties">
+ <element name="properties">
+ <interleave>
+ <optional>
+ <ref name="help"/>
+ </optional>
+ <optional>
+ <ref name="constraint"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="valueHelp"/>
+ </zeroOrMore>
+ <optional>
+ <element name="constraintErrorMessage">
+ <text/>
+ </element>
+ </optional>
+ <zeroOrMore>
+ <ref name="completionHelp"/>
+ </zeroOrMore>
+ <optional>
+ <!-- These are meaningful only for leaf nodes -->
+ <group>
+ <element name="valueless">
+ <empty/>
+ </element>
+ </group>
+ </optional>
+ <optional>
+ <element name="multi">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="hidden">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="secret">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="priority">
+ <text/>
+ </element>
+ </optional>
+ <optional>
+ <!-- These are meaningful only for tag nodes -->
+ <group>
+ <element name="keepChildOrder">
+ <empty/>
+ </element>
+ </group>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!-- All nodes must have "name" attribute -->
+ <define name="nodeNameAttr">
+ <attribute name="name"/>
+ </define>
+ <!--
+ Ordinary nodes and tag nodes can have "owner" attribute.
+ Owner is the component that is notified when node changes.
+ -->
+ <define name="ownerAttr">
+ <attribute name="owner"/>
+ </define>
+ <!--
+ Tag and leaf nodes may have constraints on their names and values
+ (respectively).
+ When multiple constraints are listed, they work as logical OR
+ -->
+ <define name="constraint">
+ <element name="constraint">
+ <oneOrMore>
+ <choice>
+ <element name="regex">
+ <text/>
+ </element>
+ <ref name="validator"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!-- A constraint may also use an external validator rather than regex -->
+ <define name="validator">
+ <element name="validator">
+ <interleave>
+ <attribute name="name"/>
+ <optional>
+ <attribute name="argument"/>
+ </optional>
+ </interleave>
+ <empty/>
+ </element>
+ </define>
+ <!-- help tags contains brief description of the purpose of the node -->
+ <define name="help">
+ <element name="help">
+ <text/>
+ </element>
+ </define>
+ <!-- valueHelp tags contain information about acceptable value format -->
+ <define name="valueHelp">
+ <element name="valueHelp">
+ <interleave>
+ <element name="format">
+ <text/>
+ </element>
+ <element name="description">
+ <text/>
+ </element>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ completionHelp tags contain information about allowed values of a node that is used for generating
+ tab completion in the CLI frontend and drop-down lists in GUI frontends
+ It is only meaninful for leaf nodes
+ Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+ as a configuration path (e.g. <path>interfaces ethernet</path>),
+ or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+ -->
+ <define name="completionHelp">
+ <element name="completionHelp">
+ <interleave>
+ <zeroOrMore>
+ <element name="list">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="path">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="script">
+ <text/>
+ </element>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+</grammar>
diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc
new file mode 100644
index 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 <maintainers@vyos.net>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+# The language of this file is compact form RELAX-NG
+# http://relaxng.org/compact-tutorial-20030326.htm
+# (unless converted to XML, then just RELAX-NG :)
+
+# Interface definition starts with interfaceDefinition tag that may contain node tags
+start = element interfaceDefinition
+{
+ node*
+}
+
+# node tag may contain node, leafNode, or tagNode tags
+# Those are intermediate configuration nodes that may only contain
+# other nodes and must not have values
+node = element node
+{
+ nodeNameAttr,
+ (properties? & children? & command?)
+}
+
+# Tag nodes are containers for nodes without predefined names, like network interfaces
+# or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+# Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+# They must not contain other tag nodes
+tagNode = element tagNode
+{
+ nodeNameAttr,
+ (properties? & children & command?)
+}
+
+# Leaf nodes are terminal configuration nodes that can't have children,
+# but can have values.
+
+leafNode = element leafNode
+{
+ nodeNameAttr,
+ (command & properties)
+}
+
+# Normal and tag nodes may have children
+children = element children
+{
+ (node | tagNode | leafNode)+
+}
+
+# Nodes may have properties
+# For simplicity, any property is allowed in any node,
+# but whether they are used or not is implementation-defined
+
+
+properties = element properties
+{
+ help? &
+ completionHelp*
+}
+
+# All nodes must have "name" attribute
+nodeNameAttr = attribute name
+{
+ text
+}
+
+
+
+
+
+# help tags contains brief description of the purpose of the node
+help = element help
+{
+ text
+}
+
+command = element command
+{
+ text
+}
+
+# completionHelp tags contain information about allowed values of a node that is used for generating
+# tab completion in the CLI frontend and drop-down lists in GUI frontends
+# It is only meaninful for leaf nodes
+# Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+# as a configuration path (e.g. <path>interfaces ethernet</path>),
+# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+completionHelp = element completionHelp
+{
+ (element list { text })* &
+ (element path { text })* &
+ (element script { text })*
+}
diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng
new file mode 100644
index 0000000..e9e7887
--- /dev/null
+++ b/schema/op-mode-definition.rng
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+ <!--
+ interface_definition.rnc: VyConf reference tree XML grammar
+
+ Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ USA
+ -->
+ <!--
+ The language of this file is compact form RELAX-NG
+ http://relaxng.org/compact-tutorial-20030326.htm
+ (unless converted to XML, then just RELAX-NG :)
+ -->
+ <!-- Interface definition starts with interfaceDefinition tag that may contain node tags -->
+ <start>
+ <element name="interfaceDefinition">
+ <zeroOrMore>
+ <ref name="node"/>
+ </zeroOrMore>
+ </element>
+ </start>
+ <!--
+ node tag may contain node, leafNode, or tagNode tags
+ Those are intermediate configuration nodes that may only contain
+ other nodes and must not have values
+ -->
+ <define name="node">
+ <element name="node">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ <optional>
+ <ref name="command"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Tag nodes are containers for nodes without predefined names, like network interfaces
+ or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+ Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+ They must not contain other tag nodes
+ -->
+ <define name="tagNode">
+ <element name="tagNode">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ <optional>
+ <ref name="command"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Leaf nodes are terminal configuration nodes that can't have children,
+ but can have values.
+ -->
+ <define name="leafNode">
+ <element name="leafNode">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <ref name="command"/>
+ <ref name="properties"/>
+ </interleave>
+ </element>
+ </define>
+ <!-- Normal and tag nodes may have children -->
+ <define name="children">
+ <element name="children">
+ <oneOrMore>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ <ref name="leafNode"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!--
+ Nodes may have properties
+ For simplicity, any property is allowed in any node,
+ but whether they are used or not is implementation-defined
+ -->
+ <define name="properties">
+ <element name="properties">
+ <interleave>
+ <optional>
+ <ref name="help"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="completionHelp"/>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+ <!-- All nodes must have "name" attribute -->
+ <define name="nodeNameAttr">
+ <attribute name="name"/>
+ </define>
+ <!-- help tags contains brief description of the purpose of the node -->
+ <define name="help">
+ <element name="help">
+ <text/>
+ </element>
+ </define>
+ <define name="command">
+ <element name="command">
+ <text/>
+ </element>
+ </define>
+ <!--
+ completionHelp tags contain information about allowed values of a node that is used for generating
+ tab completion in the CLI frontend and drop-down lists in GUI frontends
+ It is only meaninful for leaf nodes
+ Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+ as a configuration path (e.g. <path>interfaces ethernet</path>),
+ or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+ -->
+ <define name="completionHelp">
+ <element name="completionHelp">
+ <interleave>
+ <zeroOrMore>
+ <element name="list">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="path">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="script">
+ <text/>
+ </element>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+</grammar>
diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates
new file mode 100755
index 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 <maintainers@vyos.net>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+import sys
+import os
+import argparse
+import copy
+import functools
+
+from lxml import etree as ET
+
+# Defaults
+
+validator_dir = "/opt/vyatta/libexec/validators"
+default_constraint_err_msg = "Invalid value"
+
+
+## Get arguments
+
+parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
+parser.add_argument('--debug', help='Enable debug information output', action='store_true')
+parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
+parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
+parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")
+
+args = parser.parse_args()
+
+input_file = args.INPUT_FILE
+schema_file = args.SCHEMA_FILE
+output_dir = args.OUTPUT_DIR
+debug = args.debug
+
+## Load and validate the inputs
+
+try:
+ xml = ET.parse(input_file)
+except Exception as e:
+ print("Failed to load interface definition file {0}".format(input_file))
+ print(e)
+ sys.exit(1)
+
+try:
+ relaxng_xml = ET.parse(schema_file)
+ validator = ET.RelaxNG(relaxng_xml)
+
+ if not validator.validate(xml):
+ print(validator.error_log)
+ print("Interface definition file {0} does not match the schema!".format(input_file))
+ sys.exit(1)
+except Exception as e:
+ print("Failed to load the XML schema {0}".format(schema_file))
+ print(e)
+ sys.exit(1)
+
+if not os.access(output_dir, os.W_OK):
+ print("The output directory {0} is not writeable".format(output_dir))
+ sys.exit(1)
+
+## If we got this far, everything must be ok and we can convert the file
+
+def make_path(l):
+ path = functools.reduce(os.path.join, l)
+ if debug:
+ print(path)
+ return path
+
+def get_properties(p):
+ props = {}
+
+ if p is None:
+ return props
+
+ # Get the help string
+ try:
+ props["help"] = p.find("help").text
+ except:
+ props["help"] = "No help available"
+
+
+ # Get the completion help strings
+ try:
+ che = p.findall("completionHelp")
+ ch = ""
+ for c in che:
+ scripts = c.findall("script")
+ paths = c.findall("path")
+ lists = c.findall("list")
+
+ # Current backend doesn't support multiple allowed: tags
+ # so we get to emulate it
+ comp_exprs = []
+ for i in lists:
+ comp_exprs.append("echo \"{0}\"".format(i.text))
+ for i in paths:
+ comp_exprs.append("/bin/cli-shell-api 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 <maintainers@vyos.net>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+import sys
+import os
+import argparse
+import copy
+import functools
+
+from lxml import etree as ET
+
+# Defaults
+
+#validator_dir = "/usr/libexec/vyos/validators"
+validator_dir = "${vyos_validators_dir}"
+default_constraint_err_msg = "Invalid value"
+
+
+## Get arguments
+
+parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
+parser.add_argument('--debug', help='Enable debug information output', action='store_true')
+parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
+parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
+parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")
+
+args = parser.parse_args()
+
+input_file = args.INPUT_FILE
+schema_file = args.SCHEMA_FILE
+output_dir = args.OUTPUT_DIR
+debug = args.debug
+
+#debug = True
+
+## Load and validate the inputs
+
+try:
+ xml = ET.parse(input_file)
+except Exception as e:
+ print("Failed to load interface definition file {0}".format(input_file))
+ print(e)
+ sys.exit(1)
+
+try:
+ relaxng_xml = ET.parse(schema_file)
+ validator = ET.RelaxNG(relaxng_xml)
+
+ if not validator.validate(xml):
+ print(validator.error_log)
+ print("Interface definition file {0} does not match the schema!".format(input_file))
+ sys.exit(1)
+except Exception as e:
+ print("Failed to load the XML schema {0}".format(schema_file))
+ print(e)
+ sys.exit(1)
+
+if not os.access(output_dir, os.W_OK):
+ print("The output directory {0} is not writeable".format(output_dir))
+ sys.exit(1)
+
+## If we got this far, everything must be ok and we can convert the file
+
+def make_path(l):
+ path = functools.reduce(os.path.join, l)
+ if debug:
+ print(path)
+ return path
+
+def get_properties(p):
+ props = {}
+
+ if p is None:
+ return props
+
+ # Get the help string
+ try:
+ props["help"] = p.find("help").text
+ except:
+ pass
+
+ # Get value help strings
+ try:
+ vhe = p.findall("valueHelp")
+ vh = []
+ for v in vhe:
+ vh.append( (v.find("format").text, v.find("description").text) )
+ props["val_help"] = vh
+ except:
+ props["val_help"] = []
+
+ # Get the constraint statements
+ error_msg = default_constraint_err_msg
+ # Get the error message if it's there
+ try:
+ error_msg = p.find("constraintErrorMessage").text
+ except:
+ pass
+
+ vce = p.find("constraint")
+ vc = []
+ if vce is not None:
+ # The old backend doesn't support multiple validators in OR mode
+ # so we emulate it
+
+ regexes = []
+ regex_elements = vce.findall("regex")
+ if regex_elements is not None:
+ regexes = list(map(lambda e: e.text.strip(), 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 <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/VyOS.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VyOS.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/VyOS"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/VyOS"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py
new file mode 100644
index 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
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'VyOSdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'VyOS.tex', 'VyOS Documentation',
+ 'VyOS maintainers and contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'vyos', 'VyOS Documentation',
+ ['VyOS maintainers and contributors'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'VyOS', 'VyOS Documentation',
+ 'VyOS maintainers and contributors', 'VyOS', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/sphinx/source/index.rst b/sphinx/source/index.rst
new file mode 100644
index 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 }' </proc/partitions
diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py
new file mode 100755
index 0000000..53ee896
--- /dev/null
+++ b/src/completion/list_dumpable_interfaces.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+
+# Extract the list of interfaces available for traffic dumps from tcpdump -D
+
+import re
+import subprocess
+
+if __name__ == '__main__':
+ out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip()
+ out = out.split("\n")
+
+ intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out))
+
+ print(intfs)
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
new file mode 100755
index 0000000..a4968c5
--- /dev/null
+++ b/src/completion/list_interfaces.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+import sys
+import argparse
+
+import vyos.interfaces
+
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-t", "--type", type=str, help="List interfaces of specific type")
+group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces")
+
+args = parser.parse_args()
+
+if args.type:
+ try:
+ interfaces = vyos.interfaces.list_interfaces_of_type(args.type)
+
+ except ValueError as e:
+ print(e, file=sys.stderr)
+ print("")
+elif args.broadcast:
+ eth = vyos.interfaces.list_interfaces_of_type("ethernet")
+ bridge = vyos.interfaces.list_interfaces_of_type("bridge")
+ bond = vyos.interfaces.list_interfaces_of_type("bonding")
+ interfaces = eth + bridge + bond
+else:
+ interfaces = vyos.interfaces.list_interfaces()
+
+print(" ".join(interfaces))
diff --git a/src/completion/list_raidset.sh b/src/completion/list_raidset.sh
new file mode 100755
index 0000000..9ff3523
--- /dev/null
+++ b/src/completion/list_raidset.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+echo -n `cat /proc/partitions | grep md | awk '{ print $4 }'`
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
new file mode 100755
index 0000000..95f6215
--- /dev/null
+++ b/src/conf_mode/bcast_relay.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import 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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+"""
+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 <http://www.gnu.org/licenses/>.
+#
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+# File: vyos-show-version
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+
+import os
+import 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 <http://www.gnu.org/licenses/>.
+
+import sys
+
+class MayaDate(object):
+ """ Converts number of days since UNIX epoch
+ to the Maya calendar date.
+
+ Ancient Maya people used three independent calendars for
+ different purposes.
+
+ The long count calendar is for recording historical events.
+ It represents the number of days passed
+ since some date in the past the Maya believed is the day
+ our world was created.
+
+ Tzolkin calendar is for religious purposes, it has
+ two independent cycles of 13 and 20 days, where 13 day
+ cycle days are numbered, and 20 day cycle days are named.
+
+ Haab calendar is for agriculture and daily life, it's a
+ 365 day calendar with 18 months 20 days each, and 5
+ nameless days.
+
+ The smallest unit of the long count calendar is one day (kin).
+
+ """
+
+ """ The long count calendar uses five different base 18 or base 20
+ cycles. Modern scholars write long count calendar dates in a dot separated format
+ from longest to shortest cycle,
+ <baktun>.<katun>.<tun>.<winal>.<kin>
+ for example, "13.0.0.9.2".
+
+ Classic version actually used by the ancient Maya wraps around
+ every 13th baktun, but modern historians often use longer cycles
+ such as piktun = 20 baktun.
+
+ """
+ kin = 1
+ winal = 20 # 20 kin
+ tun = 360 # 18 winal
+ katun = 7200 # 20 tun
+ baktun = 144000 # 20 katun
+
+ """ Tzolk'in date is composed of two independent cycles.
+ Dates repeat every 260 days, 13 Ajaw is considered the end
+ of tzolk'in.
+
+ Every day of the 20 day cycle has unique name, we number
+ them from zero so it's easier to map the remainder to day:
+ """
+ tzolkin_days = { 0: "Imix'",
+ 1: "Ik'",
+ 2: "Ak'b'al",
+ 3: "K'an",
+ 4: "Chikchan",
+ 5: "Kimi",
+ 6: "Manik'",
+ 7: "Lamat",
+ 8: "Muluk",
+ 9: "Ok",
+ 10: "Chuwen",
+ 11: "Eb'",
+ 12: "B'en",
+ 13: "Ix",
+ 14: "Men",
+ 15: "Kib'",
+ 16: "Kab'an",
+ 17: "Etz'nab'",
+ 18: "Kawak",
+ 19: "Ajaw" }
+
+ """ As said above, haab (year) has 19 months. Only 18 are
+ true months of 20 days each, the remaining 5 days called "wayeb"
+ do not really belong to any month, but we think of them as a pseudo-month
+ for convenience.
+
+ Also, note that days of the month are actually numbered from 0, not from 1,
+ it's not for technical reasons.
+ """
+ haab_months = { 0: "Pop",
+ 1: "Wo'",
+ 2: "Sip",
+ 3: "Sotz'",
+ 4: "Sek",
+ 5: "Xul",
+ 6: "Yaxk'in'",
+ 7: "Mol",
+ 8: "Ch'en",
+ 9: "Yax",
+ 10: "Sak'",
+ 11: "Keh",
+ 12: "Mak",
+ 13: "K'ank'in",
+ 14: "Muwan'",
+ 15: "Pax",
+ 16: "K'ayab",
+ 17: "Kumk'u",
+ 18: "Wayeb'" }
+
+ """ Now we need to map the beginning of UNIX epoch
+ (Jan 1 1970 00:00 UTC) to the beginning of the long count
+ calendar (0.0.0.0.0, 4 Ajaw, 8 Kumk'u).
+
+ The problem with mapping the long count calendar to
+ any other is that its start date is not known exactly.
+
+ The most widely accepted hypothesis suggests it was
+ August 11, 3114 BC gregorian date. In this case UNIX epoch
+ starts on 12.17.16.7.5, 13 Chikchan, 3 K'ank'in
+
+ It's known as Goodman-Martinez-Thompson (GMT) correlation
+ constant.
+ """
+ start_days = 1856305
+
+ """ Seconds in day, for conversion from timestamp """
+ seconds_in_day = 60 * 60 * 24
+
+ def __init__(self, timestamp):
+ if timestamp is None:
+ self.days = self.start_days
+ else:
+ self.days = self.start_days + (int(timestamp) // self.seconds_in_day)
+
+ def long_count_date(self):
+ """ Returns long count date string """
+ days = self.days
+
+ cur_baktun = days // self.baktun
+ days = days % self.baktun
+
+ cur_katun = days // self.katun
+ days = days % self.katun
+
+ cur_tun = days // self.tun
+ days = days % self.tun
+
+ cur_winal = days // self.winal
+ days = days % self.winal
+
+ cur_kin = days
+
+ longcount_string = "{0}.{1}.{2}.{3}.{4}".format( cur_baktun,
+ cur_katun,
+ cur_tun,
+ cur_winal,
+ cur_kin )
+ return(longcount_string)
+
+ def tzolkin_date(self):
+ """ Returns tzolkin date string """
+ days = self.days
+
+ """ The start date is not the beginning of both cycles,
+ it's 4 Ajaw. So we need to add 4 to the 13 days cycle day,
+ and substract 1 from the 20 day cycle to get correct result.
+ """
+ tzolkin_13 = (days + 4) % 13
+ tzolkin_20 = (days - 1) % 20
+
+ tzolkin_string = "{0} {1}".format(tzolkin_13, self.tzolkin_days[tzolkin_20])
+
+ return(tzolkin_string)
+
+ def haab_date(self):
+ """ Returns haab date string.
+
+ The time start on 8 Kumk'u rather than 0 Pop, which is
+ 17 days before the new haab, so we need to substract 17
+ from the current date to get correct result.
+ """
+ days = self.days
+
+ haab_day = (days - 17) % 365
+ haab_month = haab_day // 20
+ haab_day_of_month = haab_day % 20
+
+ haab_string = "{0} {1}".format(haab_day_of_month, self.haab_months[haab_month])
+
+ return(haab_string)
+
+ def date(self):
+ return("{0}, {1}, {2}".format( self.long_count_date(), self.tzolkin_date(), self.haab_date() ))
+
+if __name__ == '__main__':
+ try:
+ timestamp = sys.argv[1]
+ except:
+ print("Please specify timestamp in the argument")
+ sys.exit(1)
+
+ maya_date = MayaDate(timestamp)
+ print(maya_date.date())
diff --git a/src/op_mode/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 <http://www.gnu.org/licenses/>.
+#
+# File: snmp.py
+# Purpose:
+# Show SNMP community/remote hosts
+# Used by the "run show snmp community" commands.
+
+import os
+import sys
+import argparse
+
+from vyos.config import Config
+
+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 <http://www.gnu.org/licenses/>.
+#
+# File: snmp_ifmib.py
+# Purpose:
+# Show SNMP MIB information
+# Used by the "run show snmp mib" commands.
+
+import sys
+import argparse
+import netifaces
+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 <http://www.gnu.org/licenses/>.
+#
+# File: snmp_v3.py
+# Purpose:
+# Show SNMP v3 information
+# Used by the "run show snmp v3" commands.
+
+import sys
+import jinja2
+import argparse
+
+from vyos.config import Config
+
+parser = argparse.ArgumentParser(description='Retrieve SNMP v3 information')
+parser.add_argument('--all', action="store_true", help='Show all available information')
+parser.add_argument('--group', action="store_true", help='Show the list of configured groups')
+parser.add_argument('--trap', action="store_true", help='Show the list of configured targets')
+parser.add_argument('--user', action="store_true", help='Show the list of configured users')
+parser.add_argument('--view', action="store_true", help='Show the list of configured views')
+
+GROUP_OUTP_TMPL_SRC = """
+SNMPv3 Groups:
+
+ Group View
+ ----- ----
+ {% if group -%}{% for g in group -%}
+ {{ "%-20s" | format(g.name) }}{{ g.view }}({{ g.mode }})
+ {% endfor %}{% endif %}
+"""
+
+TRAPTGT_OUTP_TMPL_SRC = """
+SNMPv3 Trap-targets:
+
+ Tpap-target Port Protocol Auth Priv Type EngineID User
+ ----------- ---- -------- ---- ---- ---- -------- ----
+ {% if trap -%}{% for t in trap -%}
+ {{ "%-20s" | format(t.name) }} {{ t.port }} {{ t.proto }} {{ t.auth }} {{ t.priv }} {{ t.type }} {{ "%-32s" | format(t.engID) }} {{ t.user }}
+ {% endfor %}{% endif %}
+"""
+
+USER_OUTP_TMPL_SRC = """
+SNMPv3 Users:
+
+ User Auth Priv Mode Group
+ ---- ---- ---- ---- -----
+ {% if user -%}{% for u in user -%}
+ {{ "%-20s" | format(u.name) }}{{ u.auth }} {{ u.priv }} {{ u.mode }} {{ u.group }}
+ {% endfor %}{% endif %}
+"""
+
+VIEW_OUTP_TMPL_SRC = """
+SNMPv3 Views:
+ {% if view -%}{% for v in view %}
+ View : {{ v.name }}
+ OIDs : .{{ v.oids | join("\n .")}}
+ {% endfor %}{% endif %}
+"""
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp v3'):
+ print("SNMP v3 is not configured")
+ sys.exit(0)
+
+ data = {
+ 'group': [],
+ 'trap': [],
+ 'user': [],
+ 'view': []
+ }
+
+ if c.exists_effective('service snmp v3 group'):
+ for g in c.list_effective_nodes('service snmp v3 group'):
+ group = {
+ 'name': g,
+ 'mode': '',
+ 'view': ''
+ }
+ group['mode'] = c.return_effective_value('service snmp v3 group {0} mode'.format(g))
+ group['view'] = c.return_effective_value('service snmp v3 group {0} view'.format(g))
+
+ data['group'].append(group)
+
+ if c.exists_effective('service snmp v3 user'):
+ for u in c.list_effective_nodes('service snmp v3 user'):
+ user = {
+ 'name' : u,
+ 'mode' : '',
+ 'auth' : '',
+ 'priv' : '',
+ 'group': ''
+ }
+ user['mode'] = c.return_effective_value('service snmp v3 user {0} mode'.format(u))
+ user['auth'] = c.return_effective_value('service snmp v3 user {0} auth type'.format(u))
+ user['priv'] = c.return_effective_value('service snmp v3 user {0} privacy type'.format(u))
+ user['group'] = c.return_effective_value('service snmp v3 user {0} group'.format(u))
+
+ data['user'].append(user)
+
+ if c.exists_effective('service snmp v3 view'):
+ for v in c.list_effective_nodes('service snmp v3 view'):
+ view = {
+ 'name': v,
+ 'oids': []
+ }
+ view['oids'] = c.list_effective_nodes('service snmp v3 view {0} oid'.format(v))
+
+ data['view'].append(view)
+
+ if c.exists_effective('service snmp v3 trap-target'):
+ for t in c.list_effective_nodes('service snmp v3 trap-target'):
+ trap = {
+ 'name' : t,
+ 'port' : '',
+ 'proto': '',
+ 'auth' : '',
+ 'priv' : '',
+ 'type' : '',
+ 'engID': '',
+ 'user' : ''
+ }
+ trap['port'] = c.return_effective_value('service snmp v3 trap-target {0} port'.format(t))
+ trap['proto'] = c.return_effective_value('service snmp v3 trap-target {0} protocol'.format(t))
+ trap['auth'] = c.return_effective_value('service snmp v3 trap-target {0} auth type'.format(t))
+ trap['priv'] = c.return_effective_value('service snmp v3 trap-target {0} privacy type'.format(t))
+ trap['type'] = c.return_effective_value('service snmp v3 trap-target {0} type'.format(t))
+ trap['engID'] = c.return_effective_value('service snmp v3 trap-target {0} engineid'.format(t))
+ trap['user'] = c.return_effective_value('service snmp v3 trap-target {0} user'.format(t))
+
+ data['trap'].append(trap)
+
+ print(data)
+ if args.all:
+ # Special case, print all templates !
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.group:
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.trap:
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.user:
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.view:
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ else:
+ parser.print_help()
+
+ sys.exit(1)
diff --git a/src/op_mode/snmp_v3_showcerts.sh b/src/op_mode/snmp_v3_showcerts.sh
new file mode 100755
index 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 <http://www.gnu.org/licenses/>.
+#
+# File: vyos-show-version
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+
+import os
+import 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 <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import importlib.util
+
+
+def prepare_module(file_path='', module_name=''):
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ sys.modules[module_name] = module
diff --git a/src/tests/test_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 <http://www.gnu.org/licenses/>.
+#
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import tempfile
+import unittest
+
+from vyos import ConfigError
+try:
+ from src.conf_mode import task_scheduler
+except ModuleNotFoundError: # for unittest.main()
+ import sys
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+ from src.conf_mode import task_scheduler
+
+
+class TestUpdateCrontab(unittest.TestCase):
+
+ def test_verify(self):
+ tests = [
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': None
+ },
+ {'name': 'has_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '0 * * * *', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'has_no_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval',
+ 'tasks': [{'name': 'aaa', 'interval': '1y', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_min',
+ 'tasks': [{'name': 'aaa', 'interval': '61m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '25h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_day',
+ 'tasks': [{'name': 'aaa', 'interval': '32d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'no_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '', 'args': ''}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/aaa', 'args': ''}],
+ 'expected': ConfigError
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ if t['expected'] is not None:
+ with self.assertRaises(t['expected']):
+ task_scheduler.verify(t['tasks'])
+ else:
+ task_scheduler.verify(t['tasks'])
+
+ def test_generate(self):
+ tests = [
+ {'name': 'zero_task',
+ 'tasks': [],
+ 'expected': []
+ },
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '10h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 */10 * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_day',
+ 'tasks': [{'name': 'aaa', 'interval': '10d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 0 */10 * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'multiple_tasks',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'},
+ {'name': 'bbb', 'interval': '', 'spec': '0 0 * * *', 'executable': '/bin/ls', 'args': '-ltr'}
+ ],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"',
+ '0 0 * * * root sg vyattacfg \"/bin/ls -ltr\"']
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ task_scheduler.crontab_file = tempfile.mkstemp()[1]
+ task_scheduler.generate(t['tasks'])
+ if len(t['expected']) > 0:
+ self.assertTrue(os.path.isfile(task_scheduler.crontab_file))
+ with open(task_scheduler.crontab_file) as f:
+ actual = f.read()
+ self.assertEqual(t['expected'], actual.splitlines())
+ os.remove(task_scheduler.crontab_file)
+ else:
+ self.assertFalse(os.path.isfile(task_scheduler.crontab_file))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/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 <http://www.gnu.org/licenses/>.
+
+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 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="foo">
+ <properties>
+ <help>foo</help>
+ </properties>
+ <children>
+ <leafNode name="bar">
+ <command>/usr/bin/bar</command>
+ <properties>
+ <help>bar</help>
+ <completionHelp>
+ <list>foo bar</list>
+ <path>interfaces tunnel</path>
+ <script>/usr/bin/foo</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/tests/data/interface-definitions/test.xml b/tests/data/interface-definitions/test.xml
new file mode 100644
index 0000000..fbb302e
--- /dev/null
+++ b/tests/data/interface-definitions/test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="foo">
+ <properties>
+ <help>foo</help>
+ </properties>
+ <children>
+ <leafNode name="bar">
+ <properties>
+ <help>bar</help>
+ <valueHelp>
+ <format>bar</format>
+ <description>bar</description>
+ </valueHelp>
+ <completionHelp>
+ <list>foo bar</list>
+ <path>interfaces tunnel</path>
+ <script>/usr/bin/foo</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>