diff options
260 files changed, 11267 insertions, 5213 deletions
diff --git a/.gitignore b/.gitignore index e7c769aaa..e86ef4036 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ debian/debhelper-build-stamp debian/.debhelper/ debian/vyos-1x debian/vyos-1x-vmware +debian/vyos-1x-smoketest debian/*.postinst.debhelper debian/*.prerm.debhelper debian/*.substvars diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d8177a5f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to VyOS + +You wan't to help us improve VyOS? This is awesome. We accept any kind of Pull +Requests on GitHub. To make the life of the maintainers and you as future +contributor (or maybe maintainer) much easier we have come up with some basic +rules. Instead of copy/pasting or maintaining two instances of how to contribute +to VyOS you can find the entire process documented in our online documentation: +https://docs.vyos.io/en/latest/contributing/development.html + +Also this guide might not be complete so any PR is much appreciated. + +It might also worth browsing our blog: https://blog.vyos.io diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..4362b4915 --- /dev/null +++ b/LICENSE @@ -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! @@ -2,6 +2,9 @@ TMPL_DIR := templates-cfg OP_TMPL_DIR := templates-op BUILD_DIR := build DATA_DIR := data +SHIM_DIR := src/shim +CC := gcc +LIBS := -lzmq CFLAGS := src = $(wildcard interface-definitions/*.xml.in) @@ -88,12 +91,14 @@ op_mode_definitions: find $(CURDIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 # XXX: delete top level op mode node.def's that now live in other packages + rm -f $(OP_TMPL_DIR)/add/node.def rm -f $(OP_TMPL_DIR)/clear/node.def rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def rm -f $(OP_TMPL_DIR)/set/node.def rm -f $(OP_TMPL_DIR)/show/node.def rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/bgp/node.def rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def @@ -103,19 +108,28 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/delete/node.def rm -f $(OP_TMPL_DIR)/reset/vpn/node.def + # XXX: ping must be able to recursivly call itself as the + # options are provided from the script itself + ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ + .PHONY: component_versions .ONESHELL: component_versions: $(BUILD_DIR) $(obj) $(CURDIR)/scripts/build-component-versions $(BUILD_DIR)/interface-definitions $(DATA_DIR) +.PHONY: vyshim +vyshim: + $(MAKE) -C $(SHIM_DIR) + .PHONY: all -all: clean interface_definitions op_mode_definitions component_versions +all: clean interface_definitions op_mode_definitions component_versions vyshim .PHONY: clean clean: rm -rf $(BUILD_DIR) rm -rf $(TMPL_DIR) rm -rf $(OP_TMPL_DIR) + $(MAKE) -C $(SHIM_DIR) clean .PHONY: test test: diff --git a/data/configd-include.json b/data/configd-include.json new file mode 100644 index 000000000..0c75657e0 --- /dev/null +++ b/data/configd-include.json @@ -0,0 +1,62 @@ +[ +"bcast_relay.py", +"dhcp_relay.py", +"dhcpv6_relay.py", +"dynamic_dns.py", +"firewall_options.py", +"host_name.py", +"http-api.py", +"https.py", +"igmp_proxy.py", +"intel_qat.py", +"interfaces-bonding.py", +"interfaces-bridge.py", +"interfaces-dummy.py", +"interfaces-ethernet.py", +"interfaces-geneve.py", +"interfaces-l2tpv3.py", +"interfaces-loopback.py", +"interfaces-macsec.py", +"interfaces-openvpn.py", +"interfaces-pppoe.py", +"interfaces-pseudo-ethernet.py", +"interfaces-tunnel.py", +"interfaces-vxlan.py", +"interfaces-wireguard.py", +"interfaces-wireless.py", +"interfaces-wirelessmodem.py", +"ipsec-settings.py", +"lldp.py", +"nat.py", +"ntp.py", +"protocols_igmp.py", +"protocols_mpls.py", +"protocols_pim.py", +"protocols_rip.py", +"protocols_static_multicast.py", +"salt-minion.py", +"service_console-server.py", +"service_ids_fastnetmon.py", +"service_ipoe-server.py", +"service_mdns-repeater.py", +"service_pppoe-server.py", +"service_router-advert.py", +"ssh.py", +"system-ip.py", +"system-ipv6.py", +"system-login-banner.py", +"system-options.py", +"system-syslog.py", +"system-timezone.py", +"system-wifi-regdom.py", +"system_console.py", +"system_lcd.py", +"task_scheduler.py", +"tftp_server.py", +"vpn_l2tp.py", +"vpn_pptp.py", +"vpn_sstp.py", +"vrf.py", +"vrrp.py", +"vyos_cert.py" +]
\ No newline at end of file diff --git a/data/templates/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl index 84de5bf51..fca520efa 100644 --- a/data/templates/accel-ppp/ipoe.config.tmpl +++ b/data/templates/accel-ppp/ipoe.config.tmpl @@ -77,7 +77,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index b0ef17525..b9131684d 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -83,7 +83,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl index 370ca7946..5ad628fde 100644 --- a/data/templates/accel-ppp/pppoe.config.tmpl +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -93,7 +93,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/pptp.config.tmpl b/data/templates/accel-ppp/pptp.config.tmpl index 0bbfc13c5..e0f2c6da9 100644 --- a/data/templates/accel-ppp/pptp.config.tmpl +++ b/data/templates/accel-ppp/pptp.config.tmpl @@ -66,7 +66,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index 2c8c00023..c9e4a1d7d 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -69,7 +69,7 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor -%} acct-timeout={{ radius_acct_tmo }} diff --git a/data/templates/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl index 3d8c3fe94..d0c7d8bf9 100644 --- a/data/templates/bcast-relay/udp-broadcast-relay.tmpl +++ b/data/templates/bcast-relay/udp-broadcast-relay.tmpl @@ -4,4 +4,4 @@ {%- if description %} # Comment: {{ description }} {% endif %} -DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" +DAEMON_ARGS="{{ '-s ' + address if address is defined }} {{ instance }} {{ port }} {{ interface | join(' ') }}" diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl index 12786b777..290aefa49 100644 --- a/data/templates/dhcp-client/daemon-options.tmpl +++ b/data/templates/dhcp-client/daemon-options.tmpl @@ -1 +1,4 @@ -DHCLIENT_OPTS="-nw -cf {{ conf_file }} -pf {{ pid_file }} -lf {{ lease_file }} {{ '-S' if dhcpv6_prm_only }} {{ '-T' if dhcpv6_temporary }} {{ ifname }}" +### Autogenerated by interface.py ### + +DHCLIENT_OPTS="-nw -cf /var/lib/dhcp/dhclient_{{ifname}}.conf -pf /var/lib/dhcp/dhclient_{{ifname}}.pid -lf /var/lib/dhcp/dhclient_{{ifname}}.leases {{ifname}}" + diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index ab772b5f6..8a44a9761 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -1,17 +1,19 @@ -# generated by dhcp.py +### Autogenerated by interface.py ### + option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; timeout 60; retry 300; interface "{{ ifname }}" { - send host-name "{{ hostname }}"; - {% if client_id -%} - send dhcp-client-identifier "{{ client_id }}"; - {% endif -%} - {% if vendor_class_id -%} - send vendor-class-identifier "{{ vendor_class_id }}"; - {% endif -%} - request subnet-mask, broadcast-address, routers, domain-name-servers, - rfc3442-classless-static-routes, domain-name, interface-mtu; - require subnet-mask; + send host-name "{{ dhcp_options.host_name }}"; +{% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %} + send dhcp-client-identifier "{{ dhcp_options.client_id }}"; +{% endif %} +{% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %} + send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}"; +{% endif %} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; } + diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl index 490f14726..68f668117 100644 --- a/data/templates/dhcp-client/ipv6.tmpl +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -1,44 +1,57 @@ -# generated by dhcp.py -# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/ +### Autogenerated by interface.py ### +# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/ interface {{ ifname }} { +{% if address is defined and 'dhcpv6' in address %} request domain-name-servers; request domain-name; -{% if dhcpv6_prm_only %} +{% if dhcpv6_options is defined and dhcpv6_options.parameters_only is defined %} information-only; -{% endif %} -{% if not dhcpv6_temporary %} - send ia-na 1; # non-temporary address -{% endif %} -{% if dhcpv6_pd_interfaces %} - send ia-pd 2; # prefix delegation -{% endif %} +{% endif %} +{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %} + send ia-na 0; # non-temporary address +{% endif %} +{% if dhcpv6_options is defined and dhcpv6_options.rapid_commit is defined %} + send rapid-commit; # wait for immediate reply instead of advertisements +{% endif %} +{% endif %} +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +{% for pd in dhcpv6_options.pd %} + send ia-pd {{ pd }}; # prefix delegation #{{ pd }} +{% endfor %} +{% endif %} }; -{% if not dhcpv6_temporary %} -id-assoc na 1 { - # Identity association NA +{% if address is defined and 'dhcpv6' in address %} +{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %} +id-assoc na 0 { + # Identity association for non temporary address }; -{% endif %} +{% endif %} +{% endif %} -{% if dhcpv6_pd_interfaces %} -id-assoc pd 2 { -{% if dhcpv6_pd_length %} - prefix ::/{{ dhcpv6_pd_length }} infinity; -{% endif %} -{% for intf in dhcpv6_pd_interfaces %} - prefix-interface {{ intf.ifname }} { -{% if intf.sla_id %} - sla-id {{ intf.sla_id }}; -{% endif %} -{% if intf.sla_len %} - sla-len {{ intf.sla_len }}; -{% endif %} -{% if intf.if_id %} - ifid {{ intf.if_id }}; -{% endif %} +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +{% for pd in dhcpv6_options.pd %} +id-assoc pd {{ pd }} { +{# length got a default value #} + prefix ::/{{ dhcpv6_options.pd[pd].length }} infinity; +{% set sla_len = 64 - dhcpv6_options.pd[pd].length|int %} +{% set count = namespace(value=0) %} +{% for interface in dhcpv6_options.pd[pd].interface if dhcpv6_options.pd[pd].interface is defined %} + prefix-interface {{ interface }} { + sla-len {{ sla_len }}; +{% if dhcpv6_options.pd[pd].interface[interface].sla_id is defined and dhcpv6_options.pd[pd].interface[interface].sla_id is not none %} + sla-id {{ dhcpv6_options.pd[pd].interface[interface].sla_id }}; +{% else %} + sla-id {{ count.value }}; +{% endif %} +{% if dhcpv6_options.pd[pd].interface[interface].address is defined and dhcpv6_options.pd[pd].interface[interface].address is not none %} + ifid {{ dhcpv6_options.pd[pd].interface[interface].address }}; +{% endif %} }; -{% endfor %} +{% set count.value = count.value + 1 %} +{% endfor %} }; -{% endif %} +{% endfor %} +{% endif %} diff --git a/data/templates/dhcp-client/ipv6_new.tmpl b/data/templates/dhcp-client/ipv6_new.tmpl deleted file mode 100644 index 112431c5f..000000000 --- a/data/templates/dhcp-client/ipv6_new.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -# generated by dhcp.py -# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/ - -interface {{ ifname }} { - request domain-name-servers; - request domain-name; -{% if dhcpv6_options is defined %} -{% if dhcpv6_options.parameters_only is defined %} - information-only; -{% endif %} -{% if dhcpv6_options.temporary is not defined %} - send ia-na 1; # non-temporary address -{% endif %} -{% if dhcpv6_options.prefix_delegation is defined %} - send ia-pd 2; # prefix delegation -{% endif %} -{% endif %} -}; - -{% if dhcpv6_options is defined %} -{% if dhcpv6_options.temporary is not defined %} -id-assoc na 1 { - # Identity association NA -}; -{% endif %} - -{% if dhcpv6_options.prefix_delegation is defined %} -id-assoc pd 2 { -{% if dhcpv6_options.prefix_delegation.length is defined %} - prefix ::/{{ dhcpv6_options.prefix_delegation.length }} infinity; -{% endif %} -{% for interface in dhcpv6_options.prefix_delegation.interface %} - prefix-interface {{ interface }} { -{% if dhcpv6_options.prefix_delegation.interface[interface].sla_id is defined %} - sla-id {{ dhcpv6_options.prefix_delegation.interface[interface].sla_id }}; -{% endif %} -{% if dhcpv6_options.prefix_delegation.interface[interface].sla_len is defined %} - sla-len {{ dhcpv6_options.prefix_delegation.interface[interface].sla_len }}; -{% endif %} -{% if dhcpv6_options.prefix_delegation.interface[interface].address is defined %} - ifid {{ dhcpv6_options.prefix_delegation.interface[interface].address }}; -{% endif %} - }; -{% endfor %} -}; -{% endif %} -{% endif %} diff --git a/data/templates/frr/bgp.frr.tmpl b/data/templates/frr/bgp.frr.tmpl index cd6f31c93..d011a1e85 100644 --- a/data/templates/frr/bgp.frr.tmpl +++ b/data/templates/frr/bgp.frr.tmpl @@ -1 +1,967 @@ -!
+{% set conf_bgp = nbgp -%} +{% for asn in nbgp -%} +! +router bgp {{ asn }} + no bgp default ipv4-unicast + +{#- set 'conf_bgp[asn].parameters' as bgp_params #} +{%- set bgp_params = conf_bgp[asn].parameters %} +{%- set bgp_afi = conf_bgp[asn].address_family %} + +{#- START Global ASN address-family section; set protocol bgp xxx address-family #} +{%- if 'address_family' in conf_bgp[asn] %} +{%- for type in bgp_afi %} +{%- if type == "ipv4_unicast" %} + ! + address-family ipv4 unicast +{# need to check #} +{%- if 'aggregate_address' in bgp_afi[type] %} +{%- for ip in bgp_afi[type].aggregate_address %} +{%- if ( ('as_set' and 'summary_only') in bgp_afi[type].aggregate_address[ip] ) %} + aggregate-address {{ ip }} as-set summary-only +{%- elif 'as_set' in bgp_afi[type].aggregate_address[ip] %} + aggregate-address {{ ip }} as-set +{%- elif 'summary_only' in bgp_afi[type].aggregate_address[ip] %} + aggregate-address {{ ip }} summary-only +{%- else %} + aggregate-address {{ ip }} +{%- endif %} +{%- endfor %} +{%- endif %} +{# END aggregate address#} +{#- redistribute #} +{# need to check. dont work. + 'metric' and 'route_map' match also only 'route_map' + 'table' parameter also include in protocol, its not what I want #} +{%- if 'redistribute' in bgp_afi[type] %} +{%- if 'table' in bgp_afi[type].redistribute %} + redistribute table {{bgp_afi[type].redistribute.table}} +{%- endif %} +{%- for protocol in bgp_afi[type].redistribute %} +{%- if ( ('metric' and 'route_map') in bgp_afi[type].redistribute[protocol] ) %} + redistribute {{protocol}} metric {{bgp_afi[type].redistribute[protocol].metric}} route-map {{bgp_afi[type].redistribute[protocol].route_map}} +{%- elif 'metric' in bgp_afi[type].redistribute[protocol] %} + redistribute {{protocol}} metric {{bgp_afi[type].redistribute[protocol].metric}} +{%- elif 'route_map' in bgp_afi[type].redistribute[protocol] %} + redistribute {{protocol}} route-map {{bgp_afi[type].redistribute[protocol].route_map}} +{%- else %} + redistribute {{protocol}} +{%- endif %} +{%- endfor %} +{%- endif %} +{#- END redistribute #} + +{%- if 'network' in bgp_afi[type] %} +{%- for net in bgp_afi[type].network %} + network {{ net }} +{%- endfor %} +{%- endif %} + exit-address-family + ! +{%- endif %} + +{%- if type == "ipv6_unicast" %} + ! + address-family ipv6 unicast +{%- if 'aggregate_address' in bgp_afi[type] %} +{%- for ip in bgp_afi[type].aggregate_address %} +{%- if ( ('as_set' and 'summary_only') in bgp_afi[type].aggregate_address[ip] ) %} + aggregate-address {{ ip }} as-set summary-only +{%- elif 'as_set' in bgp_afi[type].aggregate_address[ip] %} + aggregate-address {{ ip }} as-set +{%- elif 'summary_only' in bgp_afi[type].aggregate_address[ip] %} + aggregate-address {{ ip }} summary-only +{%- else %} + aggregate-address {{ ip }} +{%- endif %} +{%- endfor %} +{%- endif %} +{# END aggregate address#} + +{#- redistribute #} +{# need to check. doesn't work. 'metric' and 'route_map' match also only 'route_map' #} +{%- if 'redistribute' in bgp_afi[type] %} +{%- if 'table' in bgp_afi[type].redistribute %} + redistribute table {{bgp_afi[type].redistribute.table}} +{%- endif %} +{%- for protocol in bgp_afi[type].redistribute %} +{%- if ( ('metric' and 'route_map') in bgp_afi[type].redistribute[protocol] ) %} + redistribute {{protocol}} metric {{bgp_afi[type].redistribute[protocol].metric}} route-map {{bgp_afi[type].redistribute[protocol].route_map}} +{%- elif 'metric' in bgp_afi[type].redistribute[protocol] %} + redistribute {{protocol}} metric {{bgp_afi[type].redistribute[protocol].metric}} +{%- elif 'route_map' in bgp_afi[type].redistribute[protocol] %} + redistribute {{protocol}} route-map {{bgp_afi[type].redistribute[protocol].route_map}} +{%- else %} + redistribute {{protocol}} +{%- endif %} +{%- endfor %} +{%- endif %} +{#- END redistribute #} + +{%- if 'network' in bgp_afi[type] %} +{%- for net in bgp_afi[type].network %} + network {{ net }} +{%- endfor %} +{%- endif %} + exit-address-family +! +{%- endif %} +{%- endfor %} +{%- endif %} +{#- END Global ASN address-family section; set protocols bgp 65001 address-family #} + +{#- set protocols nbgp xxxx maximum-paths ibgp x, Generated by default for afi_4 #} +{#- We don't have this parameter in afi_6. But this is supported in the FRR #} +{%- if 'maximum_paths' in conf_bgp[asn] %} +{%- if 'ebgp' in conf_bgp[asn].maximum_paths %} + ! + address-family ipv4 unicast + maximum-paths {{ conf_bgp[asn].maximum_paths.ebgp }} + exit-address-family + ! +{%- endif %} +{%- if 'ibgp' in conf_bgp[asn].maximum_paths %} + ! + address-family ipv4 unicast + maximum-paths ibgp {{ conf_bgp[asn].maximum_paths.ibgp }} + exit-address-family + ! +{%- endif %} +{%- endif %} + +{#- START peer-group; set protocol bgp xxx peer-group #} +{%- if 'peer_group' in conf_bgp[asn] %} +{%- for pr_group in conf_bgp[asn].peer_group %} +{%- set conf_peer_group = conf_bgp[asn].peer_group[pr_group] %} + neighbor {{pr_group}} peer-group + +{#- First parameter for peer-group - remote-as #} +{%- if 'remote_as' in conf_peer_group %} + neighbor {{ pr_group }} remote-as {{ conf_peer_group.remote_as }} +{%- endif %} + +{%- if 'bfd' in conf_peer_group %} + neighbor {{ pr_group }} bfd +{%- endif %} + +{%- if 'capability' in conf_peer_group %} +{%- if 'dynamic' in conf_peer_group.capability %} + neighbor {{ pr_group }} capability dynamic +{%- endif %} +{%- if 'extended_nexthop' in conf_peer_group.capability %} + neighbor {{ pr_group }} capability extended-nexthop +{%- endif %} +{%- endif %} + +{%- if 'description' in conf_peer_group %} + neighbor {{ pr_group }} description {{ conf_peer_group.description }} +{%- endif %} + +{%- if 'disable_capability_negotiation' in conf_peer_group %} + neighbor {{ pr_group }} disable-capability-negotiation +{%- endif %} + +{#- https://phabricator.vyos.net/T2844. 'disable-send-community' only for afi #} +{%- if 'disable_send_community' in conf_peer_group %} + ! +{%- endif %} + +{%- if 'ebgp_multihop' in conf_peer_group %} + neighbor {{ pr_group }} ebgp-multihop {{conf_peer_group.ebgp_multihop}} +{%- endif %} + +{%- if 'local_as' in conf_peer_group %} +{%- for loc_asn in conf_peer_group.local_as %} +{%- if 'no_prepend' in conf_peer_group.local_as[loc_asn] %} + neighbor {{ pr_group }} local-as {{loc_asn}} no-prepend +{%- else %} + neighbor {{ pr_group }} local-as {{loc_asn}} +{%- endif %} +{%- endfor %} +{%- endif %} + +{%- if 'override_capability' in conf_peer_group %} + neighbor {{ pr_group }} override-capability +{%- endif %} + +{%- if 'passive' in conf_peer_group %} + neighbor {{ pr_group }} passive +{%- endif %} + +{%- if 'password' in conf_peer_group %} + neighbor {{ pr_group }} password {{ conf_peer_group.password }} +{%- endif %} + +{%- if 'shutdown' in conf_peer_group %} + neighbor {{ pr_group }} shutdown +{%- endif %} + +{%- if 'ttl_security' in conf_peer_group %} +{%- if 'hops' in conf_peer_group.ttl_security %} + neighbor {{ pr_group }} ttl-security hops {{conf_peer_group.ttl_security.hops}} +{%- endif %} +{%- endif %} + +{%- if 'update_source' in conf_peer_group %} + neighbor {{ pr_group }} update-source {{ conf_peer_group.update_source }} +{%- endif %} + +{# START peer-group afi; set protocols bgp xxx peer-group FOO address-family #} +{%- if 'address_family' in conf_peer_group %} +{%- for afi in conf_peer_group.address_family %} +{%- if afi == "ipv4_unicast" %} + ! + address-family ipv4 unicast + +{%- if 'allowas_in' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'number' in conf_peer_group.address_family.ipv4_unicast.allowas_in %} + neighbor {{ pr_group }} allowas-in {{ conf_peer_group.address_family.ipv4_unicast.allowas_in.number }} +{%- else %} + neighbor {{ pr_group }} allowas-in +{%- endif %} +{%- endif %} + +{#- START Single Params for peer-group; set protocols bgp xxx peer-group FOO address-family ipv4-unicast #} + +{%- if 'remove_private_as' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} remove-private-AS +{%- endif %} + +{%- if 'route_reflector_client' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} route-reflector-client +{%- endif %} + +{%- if 'weight' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} weight {{ conf_peer_group.address_family.ipv4_unicast.weight }} +{%- endif %} +{#- END single params for peer-group #} + +{#- Checks need to be done as-path|med|next-hop #} +{%- if 'attribute_unchanged' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'as_path' in conf_peer_group.address_family.ipv4_unicast.attribute_unchanged %} + neighbor {{ pr_group }} attribute-unchanged as-path +{%- else %} + neighbor {{ pr_group }} attribute-unchanged as-path next-hop med +{%- endif %} +{%- endif %} +{#- END attribute-unchanged #} + +{%- if 'capability' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'receive' in conf_peer_group.address_family.ipv4_unicast.capability.orf.prefix_list %} + neighbor {{ pr_group }} capability orf prefix-list receive +{%- endif %} +{%- if 'send' in conf_peer_group.address_family.ipv4_unicast.capability.orf.prefix_list %} + neighbor {{ pr_group }} capability orf prefix-list send +{%- endif %} +{%- endif %} + +{%- if 'default_originate' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'route_map' in conf_peer_group.address_family.ipv4_unicast.default_originate %} + neighbor {{ pr_group }} default-originate route-map {{ conf_peer_group.address_family.ipv4_unicast.default_originate.route_map }} +{%- else %} + neighbor {{ pr_group }} default-originate +{%- endif %} +{%- endif %} + +{%- if 'distribute_list' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv4_unicast.distribute_list %} + neighbor {{ pr_group }} distribute-list {{conf_peer_group.address_family.ipv4_unicast.distribute_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv4_unicast.distribute_list %} + neighbor {{ pr_group }} distribute-list {{conf_peer_group.address_family.ipv4_unicast.distribute_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'filter_list' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv4_unicast.filter_list %} + neighbor {{ pr_group }} filter-list {{conf_peer_group.address_family.ipv4_unicast.filter_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv4_unicast.filter_list %} + neighbor {{ pr_group }} filter-list {{conf_peer_group.address_family.ipv4_unicast.filter_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'maximum_prefix' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} maximum-prefix {{ conf_peer_group.address_family.ipv4_unicast.maximum_prefix }} +{%- endif %} + +{#- https://phabricator.vyos.net/T1817 #} +{%- if 'nexthop_self' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'force' in conf_peer_group.address_family.ipv4_unicast.nexthop_self %} + neighbor {{ pr_group }} next-hop-self force + neighbor {{ pr_group }} next-hop-self +{%- else %} + neighbor {{ pr_group }} next-hop-self +{%- endif %} +{%- endif %} + +{%- if 'route_server_client' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} route-server-client +{%- endif %} + +{%- if 'route_map' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv4_unicast.route_map %} + neighbor {{ pr_group }} route-map {{conf_peer_group.address_family.ipv4_unicast.route_map.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv4_unicast.route_map %} + neighbor {{ pr_group }} route-map {{conf_peer_group.address_family.ipv4_unicast.route_map.import}} in +{%- endif %} +{%- endif %} +{%- if 'prefix_list' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv4_unicast.prefix_list %} + neighbor {{ pr_group }} prefix-list {{conf_peer_group.address_family.ipv4_unicast.prefix_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv4_unicast.prefix_list %} + neighbor {{ pr_group }} prefix-list {{conf_peer_group.address_family.ipv4_unicast.prefix_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'soft_reconfiguration' in conf_peer_group.address_family.ipv4_unicast %} +{%- if 'inbound' is defined %} + neighbor {{ pr_group }} soft-reconfiguration inbound +{%- endif %} +{%- endif %} + +{#- Need to check. https://phabricator.vyos.net/T2387#73900 #} +{%- if 'unsuppress_map' in conf_peer_group.address_family.ipv4_unicast %} + neighbor {{ pr_group }} unsuppress-map {{conf_peer_group.address_family.ipv4_unicast.unsuppress_map}} +{%- endif %} + neighbor {{ pr_group }} activate + exit-address-family + ! +{%- endif %} + +{%- if afi == "ipv6_unicast" %} + ! + address-family ipv6 unicast + +{%- if 'allowas_in' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'number' in conf_peer_group.address_family.ipv6_unicast.allowas_in %} + neighbor {{ pr_group }} allowas-in {{ conf_peer_group.address_family.ipv6_unicast.allowas_in.number }} +{%- else %} + neighbor {{ pr_group }} allowas-in +{%- endif %} +{%- endif %} + +{#- START Single Params for peer-group afi6; set protocols bgp xxx peer-group FOO address-family ipv6-unicast #} +{%- if 'remove_private_as' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} remove-private-AS +{%- endif %} + +{%- if 'route_reflector_client' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} route-reflector-client +{%- endif %} + +{%- if 'weight' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} weight {{ conf_peer_group.address_family.ipv6_unicast.weight }} +{%- endif %} +{#- END single params for peer-group afi6 #} + +{#- Checks need to be done as-path|med|next-hop #} +{%- if 'attribute_unchanged' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'as_path' in conf_peer_group.address_family.ipv6_unicast.attribute_unchanged %} + neighbor {{ pr_group }} attribute-unchanged as-path +{%- else %} + neighbor {{ pr_group }} attribute-unchanged as-path next-hop med +{%- endif %} +{%- endif %} + +{%- if 'capability' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'receive' in conf_peer_group.address_family.ipv6_unicast.capability.orf.prefix_list %} + neighbor {{ pr_group }} capability orf prefix-list receive +{%- endif %} +{%- if 'send' in conf_peer_group.address_family.ipv6_unicast.capability.orf.prefix_list %} + neighbor {{ pr_group }} capability orf prefix-list send +{%- endif %} +{%- endif %} + +{%- if 'default_originate' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'route_map' in conf_peer_group.address_family.ipv6_unicast.default_originate %} + neighbor {{ pr_group }} default-originate route-map {{ conf_peer_group.address_family.ipv6_unicast.default_originate.route_map }} +{%- else %} + neighbor {{ pr_group }} default-originate +{%- endif %} +{%- endif %} + +{%- if 'distribute_list' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv6_unicast.distribute_list %} + neighbor {{ pr_group }} distribute-list {{conf_peer_group.address_family.ipv6_unicast.distribute_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv6_unicast.distribute_list %} + neighbor {{ pr_group }} distribute-list {{conf_peer_group.address_family.ipv6_unicast.distribute_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'filter_list' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv6_unicast.filter_list %} + neighbor {{ pr_group }} filter-list {{conf_peer_group.address_family.ipv6_unicast.filter_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv6_unicast.filter_list %} + neighbor {{ pr_group }} filter-list {{conf_peer_group.address_family.ipv6_unicast.filter_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'maximum_prefix' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} maximum-prefix {{ conf_peer_group.address_family.ipv6_unicast.maximum_prefix }} +{%- endif %} + +{#- https://phabricator.vyos.net/T1817 #} +{%- if 'nexthop_self' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'force' in conf_peer_group.address_family.ipv6_unicast.nexthop_self %} + neighbor {{ pr_group }} next-hop-self force + neighbor {{ pr_group }} next-hop-self +{%- else %} + neighbor {{ pr_group }} next-hop-self +{%- endif %} +{%- endif %} + +{%- if 'route_server_client' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} route-server-client +{%- endif %} + +{%- if 'route_map' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv6_unicast.route_map %} + neighbor {{ pr_group }} route-map {{conf_peer_group.address_family.ipv6_unicast.route_map.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv6_unicast.route_map %} + neighbor {{ pr_group }} route-map {{conf_peer_group.address_family.ipv6_unicast.route_map.import}} in +{%- endif %} +{%- endif %} +{%- if 'prefix_list' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer_group.address_family.ipv6_unicast.prefix_list %} + neighbor {{ pr_group }} prefix-list {{conf_peer_group.address_family.ipv6_unicast.prefix_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer_group.address_family.ipv6_unicast.prefix_list %} + neighbor {{ pr_group }} prefix-list {{conf_peer_group.address_family.ipv6_unicast.prefix_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'soft_reconfiguration' in conf_peer_group.address_family.ipv6_unicast %} +{%- if 'inbound' is defined %} + neighbor {{ pr_group }} soft-reconfiguration inbound +{%- endif %} +{%- endif %} + +{#- Checks need to be done. https://phabricator.vyos.net/T2387#73900 #} +{%- if 'unsuppress_map' in conf_peer_group.address_family.ipv6_unicast %} + neighbor {{ pr_group }} unsuppress-map {{conf_peer_group.address_family.ipv6_unicast.unsuppress_map}} +{%- endif %} + neighbor {{ pr_group }} activate + exit-address-family + ! +{%- endif %} + +{%- endfor %} +{%- endif %} +{# END peer-group afi; set protocols bgp xxx peer-group FOO address-family #} + +{%- endfor %} +{%- endif %} +{#- END peer-group; set protocol bgp xxx peer-group #} + +{#- START peer section; set protocol bgp xxx neighbor #} +{%- for peer in conf_bgp[asn].neighbor %} +{#- set peer-group as conf_peer #} +{%- set conf_peer = conf_bgp[asn].neighbor[peer] %} + +{#- First parameter for peer-group - remote-as #} +{%- if 'remote_as' in conf_peer %} + neighbor {{ peer }} remote-as {{ conf_peer.remote_as }} +{%- endif %} + +{%- if 'advertisement_interval' in conf_peer %} + neighbor {{ peer }} advertisement-interval {{ conf_peer.advertisement_interval }} +{%- endif %} + +{%- if 'bfd' in conf_peer %} +{%- if 'check_control_plane_failure' in conf_peer.bfd %} + neighbor {{ peer }} bfd + neighbor {{ peer }} bfd check-control-plane-failure +{%- else %} + neighbor {{ peer }} bfd +{%- endif %} +{%- endif %} + +{%- if 'capability' in conf_peer %} +{%- if 'dynamic' in conf_peer.capability %} + neighbor {{ peer }} capability dynamic +{%- endif %} +{%- if 'extended_nexthop' in conf_peer.capability %} + neighbor {{ peer }} capability extended-nexthop +{%- endif %} +{%- endif %} + +{%- if 'description' in conf_peer %} + neighbor {{ peer }} description {{ conf_peer.description }} +{%- endif %} + +{%- if 'disable_capability_negotiation' in conf_peer %} + neighbor {{ peer }} disable-capability-negotiation +{%- endif %} + +{#- https://phabricator.vyos.net/T2844. 'disable-send-community' only for afi #} +{%- if 'disable_send_community' in conf_peer %} + ! +{%- endif %} + +{%- if 'ebgp_multihop' in conf_peer %} + neighbor {{ peer }} ebgp-multihop {{conf_peer.ebgp_multihop}} +{%- endif %} + +{#- Need to check. 'Peer-group' needs to define before this section #} +{%- if 'interface' in conf_peer %} +{%- if 'peer_group' in conf_peer.interface %} + neighbor {{ peer }} interface peer-group {{conf_peer.interface.peer_group}} +{%- endif %} +{%- if 'remote_as' in conf_peer.interface %} + neighbor {{ peer }} interface remote-as {{conf_peer.interface.remote_as}} +{%- endif %} +{%- if 'v6only' in conf_peer.interface %} +{%- if 'peer_group' in conf_peer.interface.v6only %} + neighbor {{ peer }} peer-group {{conf_peer.interface.peer_group}} +{%- endif %} +{%- if 'remote_as' in conf_peer.interface.v6only %} + neighbor {{ peer }} interface v6only remote-as {{conf_peer.interface.v6only.remote_as}} +{%- endif %} +{%- endif %} +{%- endif %} + +{%- if 'local_as' in conf_peer %} +{%- for loc_asn in conf_peer.local_as %} +{%- if 'no_prepend' in conf_peer.local_as[loc_asn] %} + neighbor {{ peer }} local-as {{loc_asn}} no-prepend +{%- else %} + neighbor {{ peer }} local-as {{loc_asn}} +{%- endif %} +{%- endfor %} +{%- endif %} + +{%- if 'override_capability' in conf_peer %} + neighbor {{ peer }} override-capability +{%- endif %} + +{%- if 'passive' in conf_peer %} + neighbor {{ peer }} passive +{%- endif %} + +{%- if 'password' in conf_peer %} + neighbor {{ peer }} password {{ conf_peer.password }} +{%- endif %} + +{%- if 'peer_group' in conf_peer %} + neighbor {{ peer }} peer-group {{ conf_peer.peer_group }} +{%- endif %} + +{%- if 'port' in conf_peer %} + neighbor {{ peer }} port {{ conf_peer.port }} +{%- endif %} + +{%- if 'shutdown' in conf_peer %} + neighbor {{ peer }} shutdown +{%- endif %} + +{%- if 'strict_capability_match' in conf_peer %} + neighbor {{ peer }} strict-capability-match +{%- endif %} + +{#- Need to check #} +{%- if 'timers' in conf_peer %} +{%- if ( ('connect' and 'holdtime' and 'keepalive') in conf_peer.timers ) %} + neighbor {{ peer }} timers {{conf_peer.timers.keepalive}} {{conf_peer.timers.holdtime}} + neighbor {{ peer }} timers connect {{conf_peer.timers.connect}} +{%- endif %} +{%- endif %} + +{%- if 'ttl_security' in conf_peer %} +{%- if 'hops' in conf_peer.ttl_security %} + neighbor {{ peer }} ttl-security hops {{conf_peer.ttl_security.hops}} +{%- endif %} +{%- endif %} + +{%- if 'update_source' in conf_peer %} + neighbor {{ peer }} update-source {{ conf_peer.update_source }} +{%- endif %} + +{#- START address family for peer; set protocols bgp xxx neighbor x.x.x.x address-family ipvX-unicast #} +{%- if 'address_family' in conf_peer %} +{%- for afi in conf_peer.address_family %} +{%- if afi == "ipv4_unicast" %} + ! + address-family ipv4 unicast + +{%- if 'allowas_in' in conf_peer.address_family.ipv4_unicast %} +{%- if 'number' in conf_peer.address_family.ipv4_unicast.allowas_in %} + neighbor {{ peer }} allowas-in {{ conf_peer.address_family.ipv4_unicast.allowas_in.number }} +{%- else %} + neighbor {{ peer }} allowas-in +{%- endif %} +{%- endif %} + +{#- START Single Params for neighbor; #} +{%- if 'as_override' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} as-override +{%- endif %} + +{%- if 'remove_private_as' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} remove-private-AS +{%- endif %} + +{%- if 'route_reflector_client' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} route-reflector-client +{%- endif %} + +{%- if 'weight' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} weight {{ conf_peer.address_family.ipv4_unicast.weight }} +{%- endif %} +{#- END single params for neighbor #} + +{#- Checks need to be done as-path|med|next-hop #} +{%- if 'attribute_unchanged' in conf_peer.address_family.ipv4_unicast %} +{%- if 'as_path' in conf_peer.address_family.ipv4_unicast.attribute_unchanged %} + neighbor {{ peer }} attribute-unchanged as-path +{%- else %} + neighbor {{ peer }} attribute-unchanged as-path next-hop med +{%- endif %} +{%- endif %} +{#- END attribute-unchanged #} + +{%- if 'capability' in conf_peer.address_family.ipv4_unicast %} +{%- if 'receive' in conf_peer.address_family.ipv4_unicast.capability.orf.prefix_list %} + neighbor {{ peer }} capability orf prefix-list receive +{%- endif %} +{%- if 'send' in conf_peer.address_family.ipv4_unicast.capability.orf.prefix_list %} + neighbor {{ peer }} capability orf prefix-list send +{%- endif %} +{%- endif %} + +{%- if 'default_originate' in conf_peer.address_family.ipv4_unicast %} +{%- if 'route_map' in conf_peer.address_family.ipv4_unicast.default_originate %} + neighbor {{ peer }} default-originate route-map {{ conf_peer.address_family.ipv4_unicast.default_originate.route_map }} +{%- else %} + neighbor {{ peer }} default-originate +{%- endif %} +{%- endif %} + +{%- if 'distribute_list' in conf_peer.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer.address_family.ipv4_unicast.distribute_list %} + neighbor {{ peer }} distribute-list {{conf_peer.address_family.ipv4_unicast.distribute_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv4_unicast.distribute_list %} + neighbor {{ peer }} distribute-list {{conf_peer.address_family.ipv4_unicast.distribute_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'filter_list' in conf_peer.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer.address_family.ipv4_unicast.filter_list %} + neighbor {{ peer }} filter-list {{conf_peer.address_family.ipv4_unicast.filter_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv4_unicast.filter_list %} + neighbor {{ peer }} filter-list {{conf_peer.address_family.ipv4_unicast.filter_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'maximum_prefix' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} maximum-prefix {{ conf_peer.address_family.ipv4_unicast.maximum_prefix }} +{%- endif %} + +{#- https://phabricator.vyos.net/T1817 #} +{%- if 'nexthop_self' in conf_peer.address_family.ipv4_unicast %} +{%- if 'force' in conf_peer.address_family.ipv4_unicast.nexthop_self %} + neighbor {{ peer }} next-hop-self force + neighbor {{ peer }} next-hop-self +{%- else %} + neighbor {{ peer }} next-hop-self +{%- endif %} +{%- endif %} + +{%- if 'route_server_client' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} route-server-client +{%- endif %} + +{%- if 'route_map' in conf_peer.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer.address_family.ipv4_unicast.route_map %} + neighbor {{ peer }} route-map {{conf_peer.address_family.ipv4_unicast.route_map.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv4_unicast.route_map %} + neighbor {{ peer }} route-map {{conf_peer.address_family.ipv4_unicast.route_map.import}} in +{%- endif %} +{%- endif %} +{%- if 'prefix_list' in conf_peer.address_family.ipv4_unicast %} +{%- if 'export' in conf_peer.address_family.ipv4_unicast.prefix_list %} + neighbor {{ peer }} prefix-list {{conf_peer.address_family.ipv4_unicast.prefix_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv4_unicast.prefix_list %} + neighbor {{ peer }} prefix-list {{conf_peer.address_family.ipv4_unicast.prefix_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'soft_reconfiguration' in conf_peer.address_family.ipv4_unicast %} +{%- if 'inbound' is defined %} + neighbor {{ peer }} soft-reconfiguration inbound +{%- endif %} +{%- endif %} + +{#- Checks need to be done. https://phabricator.vyos.net/T2387#73900 #} +{%- if 'unsuppress_map' in conf_peer.address_family.ipv4_unicast %} + neighbor {{ peer }} unsuppress-map {{conf_peer.address_family.ipv4_unicast.unsuppress_map}} +{%- endif %} + neighbor {{ peer }} activate + exit-address-family + ! +{%- endif %} + +{%- if afi == "ipv6_unicast" %} + ! + address-family ipv6 unicast + +{%- if 'allowas_in' in conf_peer.address_family.ipv6_unicast %} +{%- if 'number' in conf_peer.address_family.ipv6_unicast.allowas_in %} + neighbor {{ peer }} allowas-in {{ conf_peer.address_family.ipv6_unicast.allowas_in.number }} +{%- else %} + neighbor {{ peer }} allowas-in +{%- endif %} +{%- endif %} + +{#- START Single Params for neighbor #} +{%- if 'as_override' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} as-override +{%- endif %} + +{%- if 'remove_private_as' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} remove-private-AS +{%- endif %} + +{%- if 'route_reflector_client' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} route-reflector-client +{%- endif %} + +{%- if 'weight' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} weight {{ conf_peer.address_family.ipv6_unicast.weight }} +{%- endif %} +{#- END single params for neighbor #} + +{#- Checks need to be done as-path|med|next-hop #} +{%- if 'attribute_unchanged' in conf_peer.address_family.ipv6_unicast %} +{%- if 'as_path' in conf_peer.address_family.ipv6_unicast.attribute_unchanged %} + neighbor {{ peer }} attribute-unchanged as-path +{%- else %} + neighbor {{ peer }} attribute-unchanged as-path next-hop med +{%- endif %} +{%- endif %} +{#- END attribute-unchanged #} + +{%- if 'capability' in conf_peer.address_family.ipv6_unicast %} +{%- if 'receive' in conf_peer.address_family.ipv6_unicast.capability.orf.prefix_list %} + neighbor {{ peer }} capability orf prefix-list receive +{%- endif %} +{%- if 'send' in conf_peer.address_family.ipv6_unicast.capability.orf.prefix_list %} + neighbor {{ peer }} capability orf prefix-list send +{%- endif %} +{%- endif %} + +{%- if 'default_originate' in conf_peer.address_family.ipv6_unicast %} +{%- if 'route_map' in conf_peer.address_family.ipv6_unicast.default_originate %} + neighbor {{ peer }} default-originate route-map {{ conf_peer.address_family.ipv6_unicast.default_originate.route_map }} +{%- else %} + neighbor {{ peer }} default-originate +{%- endif %} +{%- endif %} + +{%- if 'distribute_list' in conf_peer.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer.address_family.ipv6_unicast.distribute_list %} + neighbor {{ peer }} distribute-list {{conf_peer.address_family.ipv6_unicast.distribute_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv6_unicast.distribute_list %} + neighbor {{ peer }} distribute-list {{conf_peer.address_family.ipv6_unicast.distribute_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'filter_list' in conf_peer.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer.address_family.ipv6_unicast.filter_list %} + neighbor {{ peer }} filter-list {{conf_peer.address_family.ipv6_unicast.filter_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv6_unicast.filter_list %} + neighbor {{ peer }} filter-list {{conf_peer.address_family.ipv6_unicast.filter_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'maximum_prefix' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} maximum-prefix {{ conf_peer.address_family.ipv6_unicast.maximum_prefix }} +{%- endif %} + +{#- https://phabricator.vyos.net/T1817 #} +{%- if 'nexthop_self' in conf_peer.address_family.ipv6_unicast %} +{%- if 'force' in conf_peer.address_family.ipv6_unicast.nexthop_self %} + neighbor {{ peer }} next-hop-self force + neighbor {{ peer }} next-hop-self +{%- else %} + neighbor {{ peer }} next-hop-self +{%- endif %} +{%- endif %} + +{%- if 'route_server_client' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} route-server-client +{%- endif %} + +{%- if 'route_map' in conf_peer.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer.address_family.ipv6_unicast.route_map %} + neighbor {{ peer }} route-map {{conf_peer.address_family.ipv6_unicast.route_map.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv6_unicast.route_map %} + neighbor {{ peer }} route-map {{conf_peer.address_family.ipv6_unicast.route_map.import}} in +{%- endif %} +{%- endif %} +{%- if 'prefix_list' in conf_peer.address_family.ipv6_unicast %} +{%- if 'export' in conf_peer.address_family.ipv6_unicast.prefix_list %} + neighbor {{ peer }} prefix-list {{conf_peer.address_family.ipv6_unicast.prefix_list.export}} out +{%- endif %} +{%- if 'import' in conf_peer.address_family.ipv6_unicast.prefix_list %} + neighbor {{ peer }} prefix-list {{conf_peer.address_family.ipv6_unicast.prefix_list.import}} in +{%- endif %} +{%- endif %} + +{%- if 'soft_reconfiguration' in conf_peer.address_family.ipv6_unicast %} +{%- if 'inbound' is defined %} + neighbor {{ peer }} soft-reconfiguration inbound +{%- endif %} +{%- endif %} + +{#- Checks need to be done. https://phabricator.vyos.net/T2387#73900 #} +{%- if 'unsuppress_map' in conf_peer.address_family.ipv6_unicast %} + neighbor {{ peer }} unsuppress-map {{conf_peer.address_family.ipv6_unicast.unsuppress_map}} +{%- endif %} + neighbor {{ peer }} activate + exit-address-family + ! +{%- endif %} + +{%- endfor %} +{%- endif %} +{#- END address family for peer #} + +{%- endfor %} +{#- END peer section; set protocols bgp xxx neighbor #} + +{#- START parameters section; set protocol bgp xxx parameters #} +{%- if 'always_compare_med' in bgp_params %} + bgp always-compare-med +{%- endif %} + +{%- if 'bestpath' in bgp_params %} +{%- if 'compare_routerid' in bgp_params.bestpath %} + bgp bestpath compare-routerid +{%- endif %} +{%- if 'as_path' in bgp_params.bestpath %} +{%- if 'confed' in bgp_params.bestpath.as_path %} + bgp bestpath as-path confed +{%- endif %} +{%- if 'ignore' in bgp_params.bestpath.as_path %} + bgp bestpath as-path ignore +{%- endif %} +{%- if 'multipath_relax' in bgp_params.bestpath.as_path %} + bgp bestpath as-path multipath-relax +{%- endif %} +{%- endif %} +{%- if 'med' in bgp_params.bestpath %} +{%- if ( ('confed' and 'missing_as_worst') in bgp_params.bestpath.med ) %} + bgp bestpath med confed missing-as-worst +{%- elif 'confed' in bgp_params.bestpath.med %} + bgp bestpath med confed +{%- elif 'missing_as_worst' in bgp_params.bestpath.med %} + bgp bestpath med missing-as-worst +{%- endif%} +{%- endif %} +{%- endif %} + +{%- if 'cluster_id' in bgp_params %} + bgp cluster-id {{ bgp_params.cluster_id }} +{%- endif %} + +{%- if 'confederation' in bgp_params %} +{%- if 'identifier' in bgp_params.confederation %} + bgp confederation identifier {{ bgp_params.confederation.identifier }} +{%- endif %} +{%- if 'peers' in bgp_params.confederation %} + bgp confederation peers {{ bgp_params.confederation.peers }} +{%- endif %} +{%- endif %} + +{#- Doesn't work in current FRR configuration (bgp dampening 16 751 2001 61) #} +{%- if 'dampening' in bgp_params %} +{%- if ( ('half_life' and 'max_suppress_time' and 're_use' and 'start_suppress_time') in bgp_params.dampening ) %} + bgp dampening {{ bgp_params.dampening.half_life }} {{ bgp_params.dampening.re_use }} {{ bgp_params.dampening.start_suppress_time }} {{ bgp_params.dampening.max_suppress_time }} +{%- endif %} +{%- endif %} + +{%- if 'default' in bgp_params %} +{%- if 'local_pref' in bgp_params.default %} + bgp default local-preference {{ bgp_params.default.local_pref }} +{%- endif %} +{#- We use this is parameter as default in template (5-th string) #} +{%- if 'no_ipv4_unicast' in bgp_params.default %} + no bgp default ipv4-unicast +{%- endif %} +{%- endif %} + +{%- if 'deterministic_med' in bgp_params %} + bgp deterministic-med +{%- endif %} + +{%- if 'distance' in bgp_params %} +{%- if 'global' in bgp_params.distance %} +{%- if ( ('external' and 'internal' and 'local') in bgp_params.distance.global ) %} + ! + address-family ipv4 unicast + distance bgp {{ bgp_params.distance.global.external }} {{ bgp_params.distance.global.internal }} {{ bgp_params.distance.global.local }} + exit-address-family +! +{%- endif %} +{%- endif %} +{%- if 'prefix' in bgp_params.distance %} + ! + address-family ipv4 unicast +{%- for prfx in bgp_params.distance.prefix %} + distance {{ bgp_params.distance.prefix[prfx].distance }} {{ prfx }} +{%- endfor %} + exit-address-family +! +{%- endif %} +{%- endif %} + +{%- if 'graceful_restart' in bgp_params %} +{%- if 'stalepath_time' in bgp_params.graceful_restart %} + bgp graceful-restart stalepath-time {{ bgp_params.graceful_restart.stalepath_time }} +{%- endif %} +{%- endif %} + +{%- if 'log_neighbor_changes' in bgp_params %} + bgp log-neighbor-changes +{%- endif %} + +{%- if 'network_import_check' in bgp_params %} + bgp network import-check +{%- endif %} + +{%- if 'no_client_to_client_reflection' in bgp_params %} + no bgp client-to-client reflection +{%- endif %} + +{%- if 'no_fast_external_failover' in bgp_params %} + no bgp fast-external-failover +{%- endif %} + +{#- END parameters; set protocols bgp xxx parameters #} + +{%- if 'timers' in conf_bgp[asn] %} +{%- if ( ('holdtime' and 'keepalive') in conf_bgp[asn].timers ) %} + timers bgp {{conf_bgp[asn].timers.keepalive}} {{conf_bgp[asn].timers.holdtime}} +{%- endif %} +{%- endif %} + +{%- if 'route_map' in conf_bgp[asn] %} +! +ip protocol bgp route-map {{conf_bgp[asn].route_map}} +{%- endif %} +! +{%- endfor -%} +{#- END asn; router bgp xxx #} diff --git a/data/templates/frr/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl index bbff88ae5..dbaa917e8 100644 --- a/data/templates/frr/ldpd.frr.tmpl +++ b/data/templates/frr/ldpd.frr.tmpl @@ -21,6 +21,18 @@ no discovery transport-address {{ old_ldp.d_transp_ipv4 }} {% if ldp.d_transp_ipv4 -%} discovery transport-address {{ ldp.d_transp_ipv4 }} {% endif -%} +{% if old_ldp.hello_holdtime -%} +no discovery hello holdtime {{ old_ldp.hello_holdtime }} +{% endif -%} +{% if ldp.hello_holdtime -%} +discovery hello holdtime {{ ldp.hello_holdtime }} +{% endif -%} +{% if old_ldp.hello_interval -%} +no discovery hello interval {{ old_ldp.hello_interval }} +{% endif -%} +{% if ldp.hello_interval -%} +discovery hello interval {{ ldp.hello_interval }} +{% endif -%} {% for interface in old_ldp.interfaces -%} no interface {{interface}} {% endfor -%} diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index f4f2c1848..a20be45ae 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -25,10 +25,10 @@ server { {% endfor %} {% if server.certbot %} - ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + ssl_certificate {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem; + ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem; + include {{ server.certbot_dir }}/options-ssl-nginx.conf; + ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem; {% elif server.vyos_cert %} include {{ server.vyos_cert.conf }}; {% else %} @@ -52,7 +52,7 @@ server { error_page 501 502 503 =200 @50*_json; -{% if api_somewhere %} +{% if api_set %} location @50*_json { default_type application/json; return 200 '{"error": "service https api unavailable at this proxy address: set service https api-restrict virtual-host"}'; diff --git a/data/templates/lcd/LCDd.conf.tmpl b/data/templates/lcd/LCDd.conf.tmpl new file mode 100644 index 000000000..6cf6a440f --- /dev/null +++ b/data/templates/lcd/LCDd.conf.tmpl @@ -0,0 +1,132 @@ +### Autogenerted by system-display.py ## + +# LCDd.conf -- configuration file for the LCDproc server daemon LCDd +# +# This file contains the configuration for the LCDd server. +# +# The format is ini-file-like. It is divided into sections that start at +# markers that look like [section]. Comments are all line-based comments, +# and are lines that start with '#' or ';'. +# +# The server has a 'central' section named [server]. For the menu there is +# a section called [menu]. Further each driver has a section which +# defines how the driver acts. +# +# The drivers are activated by specifying them in a driver= line in the +# server section, like: +# +# Driver=curses +# +# This tells LCDd to use the curses driver. +# The first driver that is loaded and is capable of output defines the +# size of the display. The default driver to use is curses. +# If the driver is specified using the -d <driver> command line option, +# the Driver= options in the config file are ignored. +# +# The drivers read their own options from the respective sections. + +## Server section with all kinds of settings for the LCDd server ## +[server] + +# Where can we find the driver modules ? +# NOTE: Always place a slash as last character ! +DriverPath=/usr/lib/x86_64-linux-gnu/lcdproc/ + +# Tells the server to load the given drivers. Multiple lines can be given. +# The name of the driver is case sensitive and determines the section +# where to look for further configuration options of the specific driver +# as well as the name of the dynamic driver module to load at runtime. +# The latter one can be changed by giving a File= directive in the +# driver specific section. +# +# The following drivers are supported: +# bayrad, CFontz, CFontzPacket, curses, CwLnx, ea65, EyeboxOne, futaba, +# g15, glcd, glcdlib, glk, hd44780, icp_a106, imon, imonlcd,, IOWarrior, +# irman, joy, lb216, lcdm001, lcterm, linux_input, lirc, lis, MD8800, +# mdm166a, ms6931, mtc_s16209x, MtxOrb, mx5000, NoritakeVFD, +# Olimex_MOD_LCD1x9, picolcd, pyramid, rawserial, sdeclcd, sed1330, +# sed1520, serialPOS, serialVFD, shuttleVFD, sli, stv5730, svga, t6963, +# text, tyan, ula200, vlsys_m428, xosd, yard2LCD + +{% if model is defined %} +{% if model.startswith('cfa-') %} +Driver=CFontzPacket +{% elif model == 'sdec' %} +Driver=sdeclcd +{% endif %} +{% endif %} + +# Tells the driver to bind to the given interface. [default: 127.0.0.1] +Bind=127.0.0.1 + +# Listen on this specified port. [default: 13666] +Port=13666 + +# Sets the reporting level; defaults to warnings and errors only. +# [default: 2; legal: 0-5] +ReportLevel=3 + +# Should we report to syslog instead of stderr? [default: no; legal: yes, no] +ReportToSyslog=yes + +# User to run as. LCDd will drop its root privileges and run as this user +# instead. [default: nobody] +User=nobody + +# The server will stay in the foreground if set to yes. +# [default: no, legal: yes, no] +Foreground=yes + +# Hello message: each entry represents a display line; default: builtin +Hello="Starting VyOS..." + +# GoodBye message: each entry represents a display line; default: builtin +GoodBye="VyOS shutdown..." + +# Sets the interval in microseconds for updating the display. +# [default: 125000 meaning 8Hz] +FrameInterval=500000 # 2 updates per second + +# Sets the default time in seconds to displays a screen. [default: 4] +WaitTime=1 + +# If set to no, LCDd will start with screen rotation disabled. This has the +# same effect as if the ToggleRotateKey had been pressed. Rotation will start +# if the ToggleRotateKey is pressed. Note that this setting does not turn off +# priority sorting of screens. [default: on; legal: on, off] +AutoRotate=on + +# If yes, the the serverscreen will be rotated as a usual info screen. If no, +# it will be a background screen, only visible when no other screens are +# active. The special value 'blank' is similar to no, but only a blank screen +# is displayed. [default: on; legal: on, off, blank] +ServerScreen=blank + +# Set master backlight setting. If set to 'open' a client may control the +# backlight for its own screens (only). [default: open; legal: off, open, on] +Backlight=on + +# Set master heartbeat setting. If set to 'open' a client may control the +# heartbeat for its own screens (only). [default: open; legal: off, open, on] +Heartbeat=off + +# set title scrolling speed [default: 10; legal: 0-10] +TitleSpeed=10 + +{% if model is defined and model is not none %} +{% if model.startswith('cfa-') %} +## CrystalFontz packet driver (for CFA533, CFA631, CFA633 & CFA635) ## +[CFontzPacket] +Model={{ model.split('-')[1] }} +Device={{ device }} +Contrast=350 +Brightness=500 +OffBrightness=50 +Reboot=yes +USB=yes +{% elif model == 'sdec' %} +## SDEC driver for Lanner, Watchguard, Sophos sppliances ## +[sdeclcd] +# No options +{% endif %} +{% endif %} diff --git a/data/templates/lcd/lcdproc.conf.tmpl b/data/templates/lcd/lcdproc.conf.tmpl new file mode 100644 index 000000000..c79f3cd0d --- /dev/null +++ b/data/templates/lcd/lcdproc.conf.tmpl @@ -0,0 +1,60 @@ +### autogenerated by system-lcd.py ### + +# LCDproc client configuration file + +[lcdproc] +Server=127.0.0.1 +Port=13666 + +# set reporting level +ReportLevel=3 + +# report to to syslog ? +ReportToSyslog=true + +Foreground=yes + +[CPU] +Active=true +OnTime=1 +OffTime=2 +ShowInvisible=false + +[SMP-CPU] +Active=false + +[Memory] +Active=false + +[Load] +Active=false + +[Uptime] +Active=true + +[ProcSize] +Active=false + +[Disk] +Active=false + +[About] +Active=false + +[TimeDate] +Active=true +TimeFormat="%H:%M:%S" + +[OldTime] +Active=false + +[BigClock] +Active=false + +[MiniClock] +Active=false + +# Display the title bar in two-line mode. Note that with four lines or more +# the title is always shown. [default: true; legal: true, false] +ShowTitle=false + diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl index 3fc4db67e..80f4ab047 100644 --- a/data/templates/mdns-repeater/mdns-repeater.tmpl +++ b/data/templates/mdns-repeater/mdns-repeater.tmpl @@ -1,2 +1,2 @@ ### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interfaces | join(' ') }}" +DAEMON_ARGS="{{ interface | join(' ') }}" diff --git a/data/templates/ntp/ntp.conf.tmpl b/data/templates/ntp/ntp.conf.tmpl index 1c51929fd..df8157a41 100644 --- a/data/templates/ntp/ntp.conf.tmpl +++ b/data/templates/ntp/ntp.conf.tmpl @@ -19,29 +19,21 @@ restrict -6 ::1 {% set options = options + 'noselect ' if server[srv].noselect is defined else '' %} {% set options = options + 'preempt ' if server[srv].preempt is defined else '' %} {% set options = options + 'prefer ' if server[srv].prefer is defined else '' %} -server {{ srv }} iburst {{ options }} +server {{ srv | replace('_', '-') }} iburst {{ options }} {% endfor %} {% endif %} {% if allow_clients is defined and allow_clients.address is defined %} # Allowed clients configuration -{% if allow_clients.address is string %} -restrict {{ allow_clients.address|address_from_cidr }} mask {{ allow_clients.address|netmask_from_cidr }} nomodify notrap nopeer -{% else %} -{% for address in allow_clients.address %} +{% for address in allow_clients.address %} restrict {{ address|address_from_cidr }} mask {{ address|netmask_from_cidr }} nomodify notrap nopeer -{% endfor %} -{% endif %} +{% endfor %} {% endif %} {% if listen_address %} # NTP should listen on configured addresses only interface ignore wildcard -{% if listen_address is string %} -interface listen {{ listen_address }} -{% else %} -{% for address in listen_address %} +{% for address in listen_address %} interface listen {{ address }} -{% endfor %} -{% endif %} +{% endfor %} {% endif %} diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl new file mode 100644 index 000000000..328af0c0d --- /dev/null +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -0,0 +1,82 @@ +### generated by vpn_openconnect.py ### + +tcp-port = {{ listen_ports.tcp }} +udp-port = {{ listen_ports.udp }} + +run-as-user = nobody +run-as-group = daemon + +{% if "radius" in authentication.mode %} +auth = "radius [config=/run/ocserv/radiusclient.conf]" +{% else %} +auth = "plain[/run/ocserv/ocpasswd]" +{% endif %} + +{% if ssl.cert_file %} +server-cert = {{ ssl.cert_file }} +{% endif %} + +{% if ssl.key_file %} +server-key = {{ ssl.key_file }} +{% endif %} + +{% if ssl.ca_cert_file %} +ca-cert = {{ ssl.ca_cert_file }} +{% endif %} + +socket-file = /run/ocserv/ocserv.socket +occtl-socket-file = /run/ocserv/occtl.socket +use-occtl = true +isolate-workers = true +keepalive = 300 +dpd = 60 +mobile-dpd = 300 +switch-to-tcp-timeout = 30 +tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128" +auth-timeout = 240 +idle-timeout = 1200 +mobile-idle-timeout = 1800 +min-reauth-time = 3 +cookie-timeout = 300 +rekey-method = ssl +try-mtu-discovery = true +cisco-client-compat = true +dtls-legacy = true + + +# The name to use for the tun device +device = sslvpn + +# An alternative way of specifying the network: +{% if network_settings %} +# DNS settings +{% if network_settings.name_server is string %} +dns = {{ network_settings.name_server }} +{% else %} +{% for dns in network_settings.name_server %} +dns = {{ dns }} +{% endfor %} +{% endif %} +# IPv4 network pool +{% if network_settings.client_ip_settings %} +{% if network_settings.client_ip_settings.subnet %} +ipv4-network = {{ network_settings.client_ip_settings.subnet }} +{% endif %} +{% endif %} +# IPv6 network pool +{% if network_settings.client_ipv6_pool %} +{% if network_settings.client_ipv6_pool.prefix %} +ipv6-network = {{ network_settings.client_ipv6_pool.prefix }} +ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }} +{% endif %} +{% endif %} +{% endif %} + +{% if network_settings.push_route is string %} +route = {{ network_settings.push_route }} +{% else %} +{% for route in network_settings.push_route %} +route = {{ route }} +{% endfor %} +{% endif %} + diff --git a/data/templates/ocserv/ocserv_passwd.tmpl b/data/templates/ocserv/ocserv_passwd.tmpl new file mode 100644 index 000000000..ffadb4860 --- /dev/null +++ b/data/templates/ocserv/ocserv_passwd.tmpl @@ -0,0 +1,6 @@ +#<username>:<group>:<hash> +{% for user in username if username is defined %} +{% if not "disable" in username[user] %} +{{ user }}:*:{{ username[user].hash }} +{% endif %} +{% endfor %}
\ No newline at end of file diff --git a/data/templates/ocserv/radius_conf.tmpl b/data/templates/ocserv/radius_conf.tmpl new file mode 100644 index 000000000..1712d83ef --- /dev/null +++ b/data/templates/ocserv/radius_conf.tmpl @@ -0,0 +1,22 @@ +### generated by vpn_openconnect.py ### +nas-identifier VyOS +{% for srv in server %} +{% if not "disable" in server[srv] %} +{% if "port" in server[srv] %} +authserver {{ srv }}:{{server[srv]["port"]}} +{% else %} +authserver {{ srv }} +{% endif %} +{% endif %} +{% endfor %} +radius_timeout {{ timeout }} +{% if source_address %} +bindaddr {{ source_address }} +{% else %} +bindaddr * +{% endif %} +servers /run/ocserv/radius_servers +dictionary /etc/radcli/dictionary +default_realm +radius_retries 3 +#
\ No newline at end of file diff --git a/data/templates/ocserv/radius_servers.tmpl b/data/templates/ocserv/radius_servers.tmpl new file mode 100644 index 000000000..7bacac992 --- /dev/null +++ b/data/templates/ocserv/radius_servers.tmpl @@ -0,0 +1,7 @@ +### generated by vpn_openconnect.py ### +# server key +{% for srv in server %} +{% if not "disable" in server[srv] %} +{{ srv }} {{ server[srv].key }} +{% endif %} +{% endfor %} diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl index 7b1952a80..c2d0cd09a 100644 --- a/data/templates/pppoe/ip-down.script.tmpl +++ b/data/templates/pppoe/ip-down.script.tmpl @@ -30,7 +30,7 @@ vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}" {% endif %} {% endif %} -{% if dhcpv6_options is defined and dhcpv6_options.prefix_delegation is defined %} +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} # Stop wide dhcpv6 client systemctl stop dhcp6c@{{ ifname }}.service {% endif %} diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl index 3dee3d011..d0a62478c 100644 --- a/data/templates/pppoe/ipv6-up.script.tmpl +++ b/data/templates/pppoe/ipv6-up.script.tmpl @@ -40,12 +40,11 @@ echo 2 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/autoconf {% endif %} -{% if dhcpv6_options is defined and dhcpv6_options.prefix_delegation is defined %} +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} # Start wide dhcpv6 client systemctl start dhcp6c@{{ ifname }}.service {% endif %} - {% if default_route != 'none' -%} # See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved # to a VRF, this is needed to properly insert the default route. diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl index 073623eac..cebfc54b5 100644 --- a/data/templates/router-advert/radvd.conf.tmpl +++ b/data/templates/router-advert/radvd.conf.tmpl @@ -1,37 +1,47 @@ ### Autogenerated by service_router-advert.py ### -{% for i in interfaces -%} -interface {{ i.name }} { +{% if interface is defined and interface is not none %} +{% for iface in interface %} +interface {{ iface }} { IgnoreIfMissing on; - AdvDefaultPreference {{ i.default_preference }}; - AdvManagedFlag {{ i.managed_flag }}; - MaxRtrAdvInterval {{ i.interval_max }}; -{% if i.interval_min %} - MinRtrAdvInterval {{ i.interval_min }}; -{% endif %} - AdvReachableTime {{ i.reachable_time }}; - AdvIntervalOpt {{ i.send_advert }}; - AdvSendAdvert {{ i.send_advert }}; -{% if i.default_lifetime %} - AdvDefaultLifetime {{ i.default_lifetime }}; -{% endif %} -{% if i.link_mtu %} - AdvLinkMTU {{ i.link_mtu }}; -{% endif %} - AdvOtherConfigFlag {{ i.other_config_flag }}; - AdvRetransTimer {{ i.retrans_timer }}; - AdvCurHopLimit {{ i.hop_limit }}; -{% for p in i.prefixes %} - prefix {{ p.prefix }} { - AdvAutonomous {{ p.autonomous_flag }}; - AdvValidLifetime {{ p.valid_lifetime }}; - AdvOnLink {{ p.on_link }}; - AdvPreferredLifetime {{ p.preferred_lifetime }}; +{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %} + AdvDefaultPreference {{ interface[iface].default_preference }}; +{% endif %} +{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %} + AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }}; +{% endif %} +{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %} + MaxRtrAdvInterval {{ interface[iface].interval.max }}; +{% endif %} +{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %} + MinRtrAdvInterval {{ interface[iface].interval.min }}; +{% endif %} +{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %} + AdvReachableTime {{ interface[iface].reachable_time }}; +{% endif %} + AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; + AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; +{% if interface[iface].default_lifetime is defined %} + AdvDefaultLifetime {{ interface[iface].default_lifetime }}; +{% endif %} +{% if interface[iface].link_mtu is defined %} + AdvLinkMTU {{ interface[iface].link_mtu }}; +{% endif %} + AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }}; + AdvRetransTimer {{ interface[iface].retrans_timer }}; + AdvCurHopLimit {{ interface[iface].hop_limit }}; +{% for prefix in interface[iface].prefix %} + prefix {{ prefix }} { + AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }}; + AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }}; + AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }}; + AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }}; }; -{% endfor %} -{% if i.name_server %} - RDNSS {{ i.name_server | join(" ") }} { +{% endfor %} +{% if interface[iface].name_server is defined %} + RDNSS {{ interface[iface].name_server | join(" ") }} { }; -{% endif %} +{% endif %} }; -{% endfor -%} +{% endfor -%} +{% endif %} diff --git a/data/templates/ssh/override.conf.tmpl b/data/templates/ssh/override.conf.tmpl index 4276366ae..843aa927b 100644 --- a/data/templates/ssh/override.conf.tmpl +++ b/data/templates/ssh/override.conf.tmpl @@ -2,9 +2,10 @@ [Unit] StartLimitIntervalSec=0 After=vyos-router.service +ConditionPathExists={{config_file}} [Service] ExecStart= -ExecStart={{vrf_command}}/usr/sbin/sshd -D $SSHD_OPTS +ExecStart={{vrf_command}}/usr/sbin/sshd -f {{config_file}} -D $SSHD_OPTS RestartSec=10 diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl index 4fde24255..52d537aca 100644 --- a/data/templates/ssh/sshd_config.tmpl +++ b/data/templates/ssh/sshd_config.tmpl @@ -37,13 +37,9 @@ PermitRootLogin no UseDNS {{ "no" if disable_host_validation is defined else "yes" }} # Specifies the port number that sshd(8) listens on -{% if port is string %} -Port {{ port }} -{% else %} -{% for value in port %} +{% for value in port %} Port {{ value }} -{% endfor %} -{% endif %} +{% endfor %} # Gives the verbosity level that is used when logging messages from sshd LogLevel {{ loglevel | upper }} @@ -53,13 +49,9 @@ PasswordAuthentication {{ "no" if disable_password_authentication is defined els {% if listen_address %} # Specifies the local addresses sshd should listen on -{% if listen_address is string %} -ListenAddress {{ listen_address }} -{% else %} -{% for address in listen_address %} +{% for address in listen_address %} ListenAddress {{ address }} -{% endfor %} -{% endif %} +{% endfor %} {% endif %} {% if ciphers %} diff --git a/data/templates/system/curlrc.tmpl b/data/templates/system/curlrc.tmpl index 675e35a0c..3e5ce801c 100644 --- a/data/templates/system/curlrc.tmpl +++ b/data/templates/system/curlrc.tmpl @@ -1,8 +1,8 @@ -{% if http_client is defined %}
-{% if http_client.source_interface is defined %}
---interface "{{ http_client.source_interface }}"
-{% endif %}
-{% if http_client.source_address is defined %}
---interface "{{ http_client.source_address }}"
-{% endif %}
-{% endif %}
+{% if http_client is defined %} +{% if http_client.source_interface is defined %} +--interface "{{ http_client.source_interface }}" +{% endif %} +{% if http_client.source_address is defined %} +--interface "{{ http_client.source_address }}" +{% endif %} +{% endif %} diff --git a/data/templates/system/ssh_config.tmpl b/data/templates/system/ssh_config.tmpl new file mode 100644 index 000000000..509bd5479 --- /dev/null +++ b/data/templates/system/ssh_config.tmpl @@ -0,0 +1,3 @@ +{% if ssh_client is defined and ssh_client.source_address is defined and ssh_client.source_address is not none %}
+BindAddress {{ ssh_client.source_address }}
+{% endif %}
diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl index b21bacc1e..91df57aab 100644 --- a/data/templates/wifi/cfg80211.conf.tmpl +++ b/data/templates/wifi/cfg80211.conf.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -options cfg80211 ieee80211_regdom={{ regdom }} -{% endif %} +{{ 'options cfg80211 ieee80211_regdom=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl index 750ad86ee..6cd125e37 100644 --- a/data/templates/wifi/crda.tmpl +++ b/data/templates/wifi/crda.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -REGDOMAIN={{ regdom }} -{% endif %} +{{ 'REGDOMAIN=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index d6068e4db..a7efee6d5 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -9,7 +9,7 @@ device_name={{ description | truncate(32, True) }} # management frames with the Host AP driver); wlan0 with many nl80211 drivers # Note: This attribute can be overridden by the values supplied with the '-i' # command line parameter. -interface={{ intf }} +interface={{ ifname }} # Driver interface type (hostap/wired/none/nl80211/bsd); # default: hostap). nl80211 is used with all Linux mac80211 drivers. @@ -28,8 +28,7 @@ logger_syslog_level=0 logger_stdout=-1 logger_stdout_level=0 -{%- if country_code %} - +{% if country_code %} # Country code (ISO/IEC 3166-1). Used to set regulatory domain. # Set as needed to indicate country in which device is operating. # This can limit available channels and transmit power. @@ -42,14 +41,12 @@ country_code={{ country_code }} ieee80211d=1 {% endif %} -{%- if ssid %} - +{% if ssid %} # SSID to be used in IEEE 802.11 management frames ssid={{ ssid }} {% endif %} -{%- if channel %} - +{% if channel %} # Channel number (IEEE 802.11) # (default: 0, i.e., not set) # Please note that some drivers do not use this value from hostapd and the @@ -61,8 +58,7 @@ ssid={{ ssid }} channel={{ channel }} {% endif %} -{%- if mode %} - +{% if mode %} # Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), # g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used # with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this @@ -71,29 +67,30 @@ channel={{ channel }} # special value "any" can be used to indicate that any support band can be used. # This special case is currently supported only with drivers with which # offloaded ACS is used. -{% if 'n' in mode -%} +{% if 'n' in mode %} hw_mode=g -{% elif 'ac' in mode -%} +{% elif 'ac' in mode %} hw_mode=a ieee80211h=1 ieee80211ac=1 -{% else -%} +{% else %} hw_mode={{ mode }} -{% endif %} +{% endif %} {% endif %} # ieee80211w: Whether management frame protection (MFP) is enabled # 0 = disabled (default) # 1 = optional # 2 = required -{% if 'disabled' in mgmt_frame_protection -%} +{% if 'disabled' in mgmt_frame_protection %} ieee80211w=0 -{% elif 'optional' in mgmt_frame_protection -%} +{% elif 'optional' in mgmt_frame_protection %} ieee80211w=1 -{% elif 'required' in mgmt_frame_protection -%} +{% elif 'required' in mgmt_frame_protection %} ieee80211w=2 {% endif %} +{% if capabilities is defined and capabilities.ht is defined %} # ht_capab: HT capabilities (list of flags) # LDPC coding capability: [LDPC] = supported # Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary @@ -127,79 +124,50 @@ ieee80211w=2 # DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) # 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) # L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% if cap_ht %} -ht_capab= -{%- endif -%} - -{%- if cap_ht_40mhz_incapable -%} -[40-INTOLERANT] -{%- endif -%} - -{%- if cap_ht_delayed_block_ack -%} -[DELAYED-BA] -{%- endif -%} - -{%- if cap_ht_dsss_cck_40 -%} -[DSSS_CCK-40] -{%- endif -%} - -{%- if cap_ht_greenfield -%} -[GF] -{%- endif -%} - -{%- if cap_ht_ldpc -%} -[LDPC] -{%- endif -%} - -{%- if cap_ht_lsig_protection -%} -[LSIG-TXOP-PROT] -{%- endif -%} - -{%- if cap_ht_max_amsdu -%} -[MAX-AMSDU-{{ cap_ht_max_amsdu }}] -{%- endif -%} - -{%- if cap_ht_smps -%} -[SMPS-{{ cap_ht_smps | upper }}] -{%- endif -%} - -{%- if cap_ht_chan_set_width -%} -{%- for csw in cap_ht_chan_set_width -%} -[{{ csw | upper }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_short_gi -%} -{%- for gi in cap_ht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_stbc_tx -%} -[TX-STBC] -{%- endif -%} -{%- if cap_ht_stbc_rx -%} -[RX-STBC{{ cap_ht_stbc_rx }}] -{%- endif %} +{% set output = '' %} +{% set output = output + '[40-INTOLERANT]' if capabilities.ht.fourtymhz_incapable is defined else '' %} +{% set output = output + '[DELAYED-BA]' if capabilities.ht.delayed_block_ack is defined else '' %} +{% set output = output + '[DSSS_CCK-40]' if capabilities.ht.dsss_cck_40 is defined else '' %} +{% set output = output + '[GF]' if capabilities.ht.greenfield is defined else '' %} +{% set output = output + '[LDPC]' if capabilities.ht.ldpc is defined else '' %} +{% set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection is defined else '' %} +{% set output = output + '[TX-STBC]' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' if capabilities.ht.max_amsdu is defined else '' %} +{% set output = output + '[SMPS-' + capabilities.ht.smps | upper + ']' if capabilities.ht.smps is defined else '' %} + +{% if capabilities.ht.channel_set_width is defined %} +{% for csw in capabilities.ht.channel_set_width %} +{% set output = output + '[' + csw | upper + ']' %} +{% endfor %} +{% endif %} -# Required for full HT and VHT functionality -wme_enabled=1 +{% if capabilities.ht.short_gi is defined %} +{% for short_gi in capabilities.ht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} -{% if cap_ht_powersave -%} +ht_capab={{ output }} + +{% if capabilities.ht.auto_powersave is defined %} # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] # Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) uapsd_advertisement_enabled=1 -{%- endif %} +{% endif %} -{% if cap_req_ht -%} +{% endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 + + +{% if capabilities is defined and capabilities.require_ht is defined %} # Require stations to support HT PHY (reject association if they do not) require_ht=1 {% endif %} -{%- if cap_vht_chan_set_width -%} -vht_oper_chwidth={{ cap_vht_chan_set_width }} -{%- endif %} - +{% if capabilities is defined and capabilities.vht is defined %} # vht_capab: VHT capabilities (list of flags) # # vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] @@ -316,133 +284,95 @@ vht_oper_chwidth={{ cap_vht_chan_set_width }} # Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] # Indicates the possibility of Tx antenna pattern change # 0 = Tx antenna pattern might change during the lifetime of an association -# 1 = Tx antenna pattern does not change during the lifetime of an association -{% if cap_vht %} -vht_capab= -{%- endif -%} - -{%- if cap_vht_max_mpdu -%} -[MAX-MPDU-{{ cap_vht_max_mpdu }}] -{%- endif -%} - -{%- if cap_vht_max_mpdu_exp -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -{%- if '2' in cap_vht_chan_set_width -%} -[VHT160] -{%- elif '3' in cap_vht_chan_set_width -%} -[VHT160-80PLUS80] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_stbc_tx -%} -[TX-STBC-2BY1] -{%- endif -%} - -{%- if cap_vht_stbc_rx -%} -[RX-STBC-{{ cap_vht_stbc_rx }}] -{%- endif -%} - -{%- if cap_vht_link_adaptation -%} -{%- if 'unsolicited' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT2] -{%- elif 'both' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT3] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_short_gi -%} -{%- for gi in cap_vht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_ldpc -%} -[RXLDPC] -{%- endif -%} - -{%- if cap_vht_tx_powersave -%} -[VHT-TXOP-PS] -{%- endif -%} - -{%- if cap_vht_vht_cf -%} -[HTC-VHT] -{%- endif -%} - -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[SU-BEAMFORMER] -{%- elif 'single-user-beamformee' in beamform -%} -[SU-BEAMFORMEE] -{%- elif 'multi-user-beamformer' in beamform -%} -[MU-BEAMFORMER] -{%- elif 'multi-user-beamformee' in beamform -%} -[MU-BEAMFORMEE] -{%- endif -%} -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_antenna_fixed -%} -[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] -{%- endif -%} - -{%- if cap_vht_antenna_cnt -%} -{%- if cap_vht_antenna_cnt|int > 1 -%} -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -{%- if cap_vht_antenna_cnt|int < 6 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] -{%- endif -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endfor -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endif -%} -{%- endif %} +# 1 = Tx antenna pattern does not change during the lifetime of an + +{% if capabilities.vht.center_channel_freq.freq_1 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ capabilities.vht.center_channel_freq.freq_1 }} +{% endif %} + +{% if capabilities.vht.center_channel_freq.freq_2 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ capabilities.vht.center_channel_freq.freq_2 }} +{% endif %} + +{% if capabilities.vht.channel_set_width is defined %} +vht_oper_chwidth={{ capabilities.vht.channel_set_width }} +{% endif %} + +{% set output = '' %} +{% set output = output + '[TX-STBC-2BY1]' if capabilities.vht.stbc.tx is defined else '' %} +{% set output = output + '[RXLDPC]' if capabilities.vht.ldpc is defined else '' %} +{% set output = output + '[VHT-TXOP-PS]' if capabilities.vht.tx_powersave is defined else '' %} +{% set output = output + '[HTC-VHT]' if capabilities.vht.vht_cf is defined else '' %} +{% set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} +{% set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} + +{% set output = output + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' if capabilities.vht.stbc.rx is defined else '' %} +{% set output = output + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' if capabilities.vht.max_mpdu is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} + +{% set output = output + '[VHT160]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' else '' %} +{% set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' else '' %} +{% set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %} +{% set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' else '' %} + +{% if capabilities.vht.short_gi is defined %} +{% for short_gi in capabilities.vht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.beamform %} +{% for beamform in capabilities.vht.beamform %} +{% set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} +{% set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} +{% set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %} +{% set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 %} +{% if capabilities.vht.beamform %} +{% if beamform == 'single-user-beamformer' %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% endif %} +{% endif %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} +{% endif %} +{% endif %} +{% endif %} + +vht_capab={{ output }} +{% endif %} # ieee80211n: Whether IEEE 802.11n (HT) is enabled # 0 = disabled (default) # 1 = enabled # Note: You will also need to enable WMM for full HT functionality. # Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. -{% if cap_req_vht -%} +{% if capabilities is defined and capabilities.require_vht is defined %} ieee80211n=0 # Require stations to support VHT PHY (reject association if they do not) require_vht=1 -{% else -%} -{% if 'n' in mode or 'ac' in mode -%} +{% else %} +{% if 'n' in mode or 'ac' in mode %} ieee80211n=1 -{% else -%} +{% else %} ieee80211n=0 -{%- endif %} -{% endif %} - -{% if cap_vht_center_freq_1 -%} -# center freq = 5 GHz + (5 * index) -# So index 42 gives center freq 5.210 GHz -# which is channel 42 in 5G band -vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} -{% endif %} - -{% if cap_vht_center_freq_2 -%} -# center freq = 5 GHz + (5 * index) -# So index 159 gives center freq 5.795 GHz -# which is channel 159 in 5G band -vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} +{% endif %} {% endif %} -{% if disable_broadcast_ssid -%} +{% if disable_broadcast_ssid is defined %} # Send empty SSID in beacons and ignore probe request frames that do not # specify full SSID, i.e., require stations to know SSID. # default: disabled (0) @@ -463,7 +393,7 @@ ignore_broadcast_ssid=1 # 2 = use external RADIUS server (accept/deny lists are searched first) macaddr_acl=0 -{% if max_stations -%} +{% if max_stations is defined %} # Maximum number of stations allowed in station table. New stations will be # rejected after the station table is full. IEEE 802.11 has a limit of 2007 # different association IDs, so this number should not be larger than that. @@ -471,13 +401,13 @@ macaddr_acl=0 max_num_sta={{ max_stations }} {% endif %} -{% if isolate_stations -%} +{% if isolate_stations is defined %} # Client isolation can be used to prevent low-level bridging of frames between # associated stations in the BSS. By default, this bridging is allowed. ap_isolate=1 {% endif %} -{% if reduce_transmit_power -%} +{% if reduce_transmit_power is defined %} # Add Power Constraint element to Beacon and Probe Response frames # This config option adds Power Constraint element when applicable and Country # element is added. Power Constraint element is required by Transmit Power @@ -486,14 +416,15 @@ ap_isolate=1 local_pwr_constraint={{ reduce_transmit_power }} {% endif %} -{% if expunge_failing_stations -%} +{% if expunge_failing_stations is defined %} # Disassociate stations based on excessive transmission failures or other # indications of connection loss. This depends on the driver capabilities and # may not be available with all drivers. disassoc_low_ack=1 {% endif %} -{% if sec_wep -%} + +{% if security is defined and security.wep is defined %} # IEEE 802.11 specifies two authentication algorithms. hostapd can be # configured to allow both of these or only one. Open system authentication # should be used with IEEE 802.1X. @@ -522,13 +453,14 @@ wep_default_key=0 # digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or # 128-bit (152-bit) WEP is used. # Only the default key must be supplied; the others are optional. -{% if sec_wep_key -%} -{% for key in sec_wep_key -%} -wep_key{{ loop.index -1 }}={{ key}} -{% endfor %} -{%- endif %} +{% if security.wep.key is defined %} +{% for key in sec_wep_key %} +wep_key{{ loop.index -1 }}={{ security.wep.key }} +{% endfor %} +{% endif %} + -{% elif sec_wpa -%} +{% elif security is defined and security.wpa is defined %} ##### WPA/IEEE 802.11i configuration ########################################## # Enable WPA. Setting this variable configures the AP to require WPA (either @@ -542,15 +474,17 @@ wep_key{{ loop.index -1 }}={{ key}} # and/or WPA2 (full IEEE 802.11i/RSN): # bit0 = WPA # bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) -{% if 'both' in sec_wpa_mode -%} +{% if security.wpa.mode is defined %} +{% if security.wpa.mode == 'both' %} wpa=3 -{%- elif 'wpa2' in sec_wpa_mode -%} +{% elif security.wpa.mode == 'wpa2' %} wpa=2 -{%- elif 'wpa' in sec_wpa_mode -%} +{% elif security.wpa.mode == 'wpa' %} wpa=1 -{%- endif %} +{% endif %} +{% endif %} -{% if sec_wpa_cipher -%} +{% if security.wpa.cipher is defined %} # Set of accepted cipher suites (encryption algorithms) for pairwise keys # (unicast packets). This is a space separated list of algorithms: # CCMP = AES in Counter mode with CBC-MAC (CCMP-128) @@ -563,26 +497,27 @@ wpa=1 # allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, # TKIP will be used as the group cipher. The optional group_cipher parameter can # be used to override this automatic selection. -{% if 'wpa2' in sec_wpa_mode -%} + +{% if security.wpa.mode is defined and security.wpa.mode == 'wpa2' %} # Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) -rsn_pairwise={{ sec_wpa_cipher | join(" ") }} -{% else -%} +rsn_pairwise={{ security.wpa.cipher | join(" ") }} +{% else %} # Pairwise cipher for WPA (v1) (default: TKIP) -wpa_pairwise={{ sec_wpa_cipher | join(" ") }} -{%- endif -%} -{% endif %} +wpa_pairwise={{ security.wpa.cipher | join(" ") }} +{% endif %} +{% endif %} -{% if sec_wpa_group_cipher -%} +{% if security.wpa.group_cipher is defined %} # Optional override for automatic group cipher selection # This can be used to select a specific group cipher regardless of which # pairwise ciphers were enabled for WPA and RSN. It should be noted that # overriding the group cipher with an unexpected value can result in # interoperability issues and in general, this parameter is mainly used for # testing purposes. -group_cipher={{ sec_wpa_group_cipher | join(" ") }} -{% endif %} +group_cipher={{ security.wpa.group_cipher | join(" ") }} +{% endif %} -{% if sec_wpa_passphrase -%} +{% if security.wpa.passphrase is defined %} # IEEE 802.11 specifies two authentication algorithms. hostapd can be # configured to allow both of these or only one. Open system authentication # should be used with IEEE 802.1X. @@ -595,7 +530,7 @@ auth_algs=1 # secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase # (8..63 characters) that will be converted to PSK. This conversion uses SSID # so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} +wpa_passphrase={{ security.wpa.passphrase }} # Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The # entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be @@ -604,7 +539,7 @@ wpa_passphrase={{ sec_wpa_passphrase }} # WPA-PSK-SHA256 = WPA2-Personal using SHA256 wpa_key_mgmt=WPA-PSK -{% elif sec_wpa_radius -%} +{% elif security.wpa.radius is defined %} ##### IEEE 802.1X-2004 related configuration ################################## # Require IEEE 802.1X authorization ieee8021x=1 @@ -616,40 +551,37 @@ ieee8021x=1 # WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 wpa_key_mgmt=WPA-EAP -{% if sec_wpa_radius_source -%} +{% if security.wpa.radius.server is defined %} # RADIUS client forced local IP address for the access point # Normally the local IP address is determined automatically based on configured # IP addresses, but this field can be used to force a specific address to be # used, e.g., when the device has multiple IP addresses. -radius_client_addr={{ sec_wpa_radius_source }} - -# The own IP address of the access point (used as NAS-IP-Address) -own_ip_addr={{ sec_wpa_radius_source }} -{% else %} # The own IP address of the access point (used as NAS-IP-Address) +{% if security.wpa.radius.source_address is defined %} +radius_client_addr={{ security.wpa.radius.source_address }} +own_ip_addr={{ security.wpa.radius.source_address }} +{% else %} own_ip_addr=127.0.0.1 -{% endif %} +{% endif %} -{% for radius in sec_wpa_radius -%} -{%- if not radius.disabled -%} +{% for radius in security.wpa.radius.server if not radius.disabled %} # RADIUS authentication server auth_server_addr={{ radius.server }} auth_server_port={{ radius.port }} auth_server_shared_secret={{ radius.key }} -{% if radius.acc_port -%} + +{% if radius.acc_port %} # RADIUS accounting server acct_server_addr={{ radius.server }} acct_server_port={{ radius.acc_port }} acct_server_shared_secret={{ radius.key }} -{% endif %} -{% endif %} -{% endfor %} - -{% endif %} - -{% else %} +{% endif %} +{% endfor %} +{% else %} # Open system auth_algs=1 +{% endif %} +{% endif %} {% endif %} # TX queue parameters (EDCF / bursting) diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl index 2784883f1..9ddad35fd 100644 --- a/data/templates/wifi/wpa_supplicant.conf.tmpl +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -1,8 +1,8 @@ # WPA supplicant config network={ ssid="{{ ssid }}" -{%- if sec_wpa_passphrase %} - psk="{{ sec_wpa_passphrase }}" +{% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %} + psk="{{ security.wpa.passphrase }}" {% else %} key_mgmt=NONE {% endif %} diff --git a/data/vyos-configd-env-set b/data/vyos-configd-env-set new file mode 100644 index 000000000..d6d421eba --- /dev/null +++ b/data/vyos-configd-env-set @@ -0,0 +1,2 @@ +# +export vyshim=/usr/sbin/vyshim diff --git a/data/vyos-configd-env-unset b/data/vyos-configd-env-unset new file mode 100644 index 000000000..9616f9858 --- /dev/null +++ b/data/vyos-configd-env-unset @@ -0,0 +1,2 @@ +# +unset vyshim diff --git a/debian/changelog b/debian/changelog index fba9d77d0..2b65b22c6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -vyos-1x (1.0.0) unstable; urgency=medium +vyos-1x (1.3dev0) unstable; urgency=medium * Dummy changelog entry for vyos-1x repository This is a internal VyOS package and the VyOS package process does not use @@ -7,4 +7,4 @@ vyos-1x (1.0.0) unstable; urgency=medium The correct verion number of this package is auto-generated by GIT on build-time - -- Runar Borge <runar@borge.nu> Sat, 9 May 2020 22:00:00 +0100 + -- VyOS maintainers and contributors <maintainers@vyos.io> Wed, 26 Aug 2020 19:07:24 +0000 diff --git a/debian/control b/debian/control index 6746fe647..d9663d07b 100644 --- a/debian/control +++ b/debian/control @@ -2,110 +2,116 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> -Build-Depends: debhelper (>= 9), - quilt, +Build-Depends: + debhelper (>= 9), + fakeroot, + libvyosconfig0 (>= 0.0.7), + libzmq3-dev, python3, - python3-setuptools, - python3-xmltodict, + python3-coverage, python3-lxml, python3-nose, - python3-coverage, - whois, - fakeroot, - libvyosconfig0 (>= 0.0.7) + python3-setuptools, + python3-xmltodict, + quilt, + whois Standards-Version: 3.9.6 Package: vyos-1x Architecture: all -Depends: python3, - ${python3:Depends}, - python3-netifaces, - python3-jinja2, - python3-pystache, - python3-psutil, - python3-tabulate, - python3-six, - python3-isc-dhcp-leases, - python3-hurry.filesize, - python3-vici (>= 5.7.2), - python3-flask, - python3-waitress, - python3-netaddr, - python3-zmq, - python3-jmespath, - python3-xmltodict, - python3-pyudev, - python3-voluptuous, +Depends: + accel-ppp, + beep, + bmon, bsdmainutils, + conntrack, + conserver-client, + conserver-server, + crda, cron, - etherwake, - systemd, + dbus, + dropbear, easy-rsa, + etherwake, + fastnetmon, + file, + frr, + frr-pythontools, + hostapd (>= 0.6.8), + hvinfo, + igmpproxy, ipaddrcheck, - tcpdump, + iperf, + iperf3, + iputils-arping, isc-dhcp-client, - wide-dhcpv6-client, - bmon, - hvinfo, + isc-dhcp-relay, + isc-dhcp-server, + iw, + keepalived (>=2.0.5), + lcdproc, + libatomic1, + libndp-tools, + libpam-radius-auth (>= 1.5.0), + libvyosconfig0, + lldpd, lm-sensors, - file, lsscsi, - pciutils, - usbutils, - procps, - snmp, snmpd, - openssh-server, + mdns-repeater, + mtr-tiny, + nftables (>= 0.9.3), + nginx-light, ntp, ntpdate, - iputils-arping, - libvyosconfig0, - beep, - dropbear, - conserver-server, - conserver-client, - isc-dhcp-server, - isc-dhcp-relay, - keepalived (>=2.0.5), - wireguard, - tftpd-hpa, - igmpproxy, - accel-ppp, - mdns-repeater, - udp-broadcast-relay, - pdns-recursor, - lcdproc, - lcdproc-extra-drivers, + ocserv, + openssh-server, openvpn, openvpn-auth-ldap, openvpn-auth-radius, - libpam-radius-auth (>= 1.5.0), - mtr-tiny, - telnet, - traceroute, - ssl-cert, - nginx-light, - lldpd, - iperf, - iperf3, - frr, - frr-pythontools, - radvd, - dbus, - usb-modeswitch, - hostapd (>= 0.6.8), - wpasupplicant (>= 0.6.7), - iw, - crda, - wireless-regdb, + pciutils, + pdns-recursor, pmacct (>= 1.6.0), - python3-certbot-nginx, pppoe, + procps, + python3, + python3-certbot-nginx, + ${python3:Depends}, + python3-flask, + python3-hurry.filesize, + python3-isc-dhcp-leases, + python3-jinja2, + python3-jmespath, + python3-netaddr, + python3-netifaces, + python3-psutil, + python3-pystache, + python3-pyudev, + python3-six, + python3-tabulate, + python3-vici (>= 5.7.2), + python3-voluptuous, + python3-waitress, + python3-xmltodict, + python3-zmq, + radvd, salt-minion, + snmp, + snmpd, + ssl-cert, + systemd, + tcpdump, + tcptraceroute, + telnet, + tftpd-hpa, + traceroute, + udp-broadcast-relay, + usb-modeswitch, + usbutils, vyos-utils, - nftables (>= 0.9.3), - conntrack, - libatomic1, - fastnetmon + wide-dhcpv6-client, + wireguard-tools, + wireless-regdb, + wpasupplicant (>= 0.6.7) Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything @@ -117,3 +123,8 @@ Depends: Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. +Package: vyos-1x-smoketest +Architecture: all +Depends: + vyos-1x +Description: VyOS build sanity checking toolkit diff --git a/debian/rules b/debian/rules index c080b8633..6b982fd8e 100755 --- a/debian/rules +++ b/debian/rules @@ -33,6 +33,7 @@ override_dh_auto_install: mkdir -p $(DIR)/$(VYOS_SBIN_DIR) mkdir -p $(DIR)/$(VYOS_BIN_DIR) cp -r src/utils/* $(DIR)/$(VYOS_BIN_DIR) + cp src/shim/vyshim $(DIR)/$(VYOS_SBIN_DIR) # Install conf mode scripts mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode @@ -91,3 +92,11 @@ override_dh_auto_install: # Make directory for generated configuration file mkdir -p $(DIR)/etc/vyos + + # Install smoke test scripts + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke/ + cp -r smoketest/scripts/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke + + # Install system programs + mkdir -p $(DIR)/$(VYOS_BIN_DIR) + cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR) diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install new file mode 100644 index 000000000..fdf949557 --- /dev/null +++ b/debian/vyos-1x-smoketest.install @@ -0,0 +1,2 @@ +usr/bin/vyos-smoketest +usr/libexec/vyos/tests/smoke diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 599f3f3f5..6d5026e91 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -7,4 +7,18 @@ etc/udev etc/vyos lib/ opt/ -usr/ +usr/sbin/vyshim +usr/bin/initial-setup +usr/bin/vyos-config-file-query +usr/bin/vyos-config-to-commands +usr/bin/vyos-config-to-json +usr/bin/vyos-hostsd-client +usr/lib +usr/libexec/vyos/completion +usr/libexec/vyos/conf_mode +usr/libexec/vyos/op_mode +usr/libexec/vyos/services +usr/libexec/vyos/system +usr/libexec/vyos/validators +usr/libexec/vyos/*.py +usr/share diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.in index 227a043cd..e37b68514 100644 --- a/interface-definitions/include/accel-radius-additions.xml.in +++ b/interface-definitions/include/accel-radius-additions.xml.in @@ -2,6 +2,18 @@ <children> <tagNode name="server"> <children> + <leafNode name="acct-port"> + <properties> + <help>Accounting port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1813)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> <leafNode name="fail-time"> <properties> <help>Mark server unavailable for <n> seconds on failure</help> diff --git a/interface-definitions/include/bgp-afi-aggregate-address.xml.i b/interface-definitions/include/bgp-afi-aggregate-address.xml.i index 40c030fc1..050ee0074 100644 --- a/interface-definitions/include/bgp-afi-aggregate-address.xml.i +++ b/interface-definitions/include/bgp-afi-aggregate-address.xml.i @@ -1,12 +1,12 @@ -<leafNode name="as-set">
- <properties>
- <help>Generate AS-set path information for this aggregate address</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="summary-only">
- <properties>
- <help>Announce the aggregate summary network only</help>
- <valueless/>
- </properties>
-</leafNode>
+<leafNode name="as-set"> + <properties> + <help>Generate AS-set path information for this aggregate address</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="summary-only"> + <properties> + <help>Announce the aggregate summary network only</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/dhcp-options.xml.i b/interface-definitions/include/dhcp-options.xml.i index 0f71d9321..9989291fc 100644 --- a/interface-definitions/include/dhcp-options.xml.i +++ b/interface-definitions/include/dhcp-options.xml.i @@ -1,6 +1,6 @@ <node name="dhcp-options"> <properties> - <help>DHCP options</help> + <help>DHCP client settings/options</help> </properties> <children> <leafNode name="client-id"> diff --git a/interface-definitions/include/dhcpv6-options.xml.i b/interface-definitions/include/dhcpv6-options.xml.i index 98a87dba2..b0a806806 100644 --- a/interface-definitions/include/dhcpv6-options.xml.i +++ b/interface-definitions/include/dhcpv6-options.xml.i @@ -1,11 +1,24 @@ <node name="dhcpv6-options"> <properties> - <help>DHCPv6 options</help> + <help>DHCPv6 client settings/options</help> </properties> <children> - <node name="prefix-delegation"> + <leafNode name="parameters-only"> <properties> - <help>DHCPv6 Prefix Delegation Options</help> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="pd"> + <properties> + <help>DHCPv6 prefix delegation interface statement</help> + <valueHelp> + <format>instance number</format> + <description>Prefix delegation instance (>= 0)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--non-negative"/> + </constraint> </properties> <children> <leafNode name="length"> @@ -19,6 +32,7 @@ <validator name="numeric" argument="--range 32-64"/> </constraint> </properties> + <defaultValue>64</defaultValue> </leafNode> <tagNode name="interface"> <properties> @@ -52,31 +66,19 @@ </constraint> </properties> </leafNode> - <leafNode name="sla-len"> - <properties> - <help>Site-Level aggregator (SLA) length</help> - <valueHelp> - <format>0-128</format> - <description>Length of delegated prefix</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-128"/> - </constraint> - </properties> - </leafNode> </children> </tagNode> </children> - </node> - <leafNode name="parameters-only"> + </tagNode> + <leafNode name="rapid-commit"> <properties> - <help>Acquire only config parameters, no address</help> + <help>Wait for immediate reply instead of advertisements</help> <valueless/> </properties> </leafNode> <leafNode name="temporary"> <properties> - <help>IPv6 "temporary" address</help> + <help>IPv6 temporary address</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/include/interface-arp-cache-timeout.xml.i b/interface-definitions/include/interface-arp-cache-timeout.xml.i index 81d35f593..e65321158 100644 --- a/interface-definitions/include/interface-arp-cache-timeout.xml.i +++ b/interface-definitions/include/interface-arp-cache-timeout.xml.i @@ -10,4 +10,5 @@ </constraint> <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> </properties> + <defaultValue>30</defaultValue> </leafNode> diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index f62a08987..a2d058479 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -2,13 +2,13 @@ <properties> <help>Rule number for NAT</help> <valueHelp> - <format>1-9999</format> + <format>1-999999</format> <description>Number for this NAT rule</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-9999"/> + <validator name="numeric" argument="--range 1-999999"/> </constraint> - <constraintErrorMessage>NAT rule number must be between 1 and 9999</constraintErrorMessage> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> <leafNode name="description"> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index ddd52979b..7d658f6a0 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -78,6 +78,7 @@ </constraint> <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage> </properties> + <defaultValue>layer2</defaultValue> </leafNode> <node name="ip"> <children> @@ -137,6 +138,7 @@ </constraint> <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage> </properties> + <defaultValue>802.3ad</defaultValue> </leafNode> <node name="member"> <properties> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 6b610e623..92356d696 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -32,6 +32,7 @@ <validator name="numeric" argument="--range 0-0 --range 10-1000000"/> </constraint> </properties> + <defaultValue>300</defaultValue> </leafNode> #include <include/interface-description.xml.i> #include <include/dhcp-options.xml.i> @@ -51,6 +52,7 @@ </constraint> <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage> </properties> + <defaultValue>14</defaultValue> </leafNode> <leafNode name="hello-time"> <properties> @@ -64,6 +66,7 @@ </constraint> <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage> </properties> + <defaultValue>2</defaultValue> </leafNode> <node name="igmp"> <properties> @@ -107,6 +110,7 @@ </constraint> <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage> </properties> + <defaultValue>20</defaultValue> </leafNode> <node name="member"> <properties> @@ -133,6 +137,7 @@ </constraint> <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage> </properties> + <defaultValue>100</defaultValue> </leafNode> <leafNode name="priority"> <properties> @@ -146,6 +151,7 @@ </constraint> <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage> </properties> + <defaultValue>32</defaultValue> </leafNode> </children> </tagNode> @@ -163,6 +169,7 @@ </constraint> <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage> </properties> + <defaultValue>32768</defaultValue> </leafNode> <leafNode name="stp"> <properties> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 1e32a15f8..e8f3f09f1 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -56,6 +56,7 @@ </constraint> <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage> </properties> + <defaultValue>auto</defaultValue> </leafNode> #include <include/interface-hw-id.xml.i> <node name="ip"> @@ -265,6 +266,7 @@ </constraint> <constraintErrorMessage>Speed must be auto, 10, 100, 1000, 2500, 5000, 10000, 25000, 40000, 50000 or 100000</constraintErrorMessage> </properties> + <defaultValue>auto</defaultValue> </leafNode> #include <include/vif-s.xml.i> #include <include/vif.xml.i> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in index 30dd9b604..3a878ad76 100644 --- a/interface-definitions/interfaces-l2tpv3.xml.in +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -29,6 +29,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>5000</defaultValue> </leafNode> #include <include/interface-disable.xml.i> <leafNode name="encapsulation"> @@ -50,6 +51,7 @@ </constraint> <constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage> </properties> + <defaultValue>udp</defaultValue> </leafNode> <node name="ipv6"> <children> @@ -138,6 +140,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>5000</defaultValue> </leafNode> <leafNode name="tunnel-id"> <properties> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index d5f9ca661..4382db598 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -70,7 +70,9 @@ </constraint> <constraintErrorMessage>mode must be private, vepa, bridge or passthru</constraintErrorMessage> </properties> + <defaultValue>private</defaultValue> </leafNode> + #include <include/interface-mtu-68-9000.xml.i> #include <include/vif-s.xml.i> #include <include/vif.xml.i> </children> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index bd3ab4022..8529f6885 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -93,6 +93,7 @@ <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> + <defaultValue>8472</defaultValue> </leafNode> <leafNode name="vni"> <properties> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 5894f159d..981bce826 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -30,9 +30,10 @@ <description>value which marks the packet for QoS/shaper</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-255"/> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="private-key"> <properties> @@ -41,6 +42,7 @@ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> </completionHelp> </properties> + <defaultValue>default</defaultValue> </leafNode> <tagNode name="peer"> <properties> @@ -103,7 +105,11 @@ #include <include/port-number.xml.i> <leafNode name="persistent-keepalive"> <properties> - <help>how often send keep alives in seconds</help> + <help>Interval to send keepalive messages</help> + <valueHelp> + <format>1-65535</format> + <description>Interval in seconds</description> + </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 06c7734f5..6f0ec9e71 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -320,7 +320,7 @@ <properties> <help>VHT link adaptation capabilities</help> <completionHelp> - <list>unsolicited both</list> + <list>unsolicited both</list> </completionHelp> <valueHelp> <format>unsolicited</format> @@ -451,6 +451,7 @@ <leafNode name="disable-broadcast-ssid"> <properties> <help>Disable broadcast of SSID from access-point</help> + <valueless/> </properties> </leafNode> #include <include/interface-disable-link-detect.xml.i> @@ -551,9 +552,10 @@ <description>802.11ac - 1300 Mbits/sec</description> </valueHelp> <constraint> - <regex>(a|b|g|n|ac)</regex> + <regex>^(a|b|g|n|ac)$</regex> </constraint> </properties> + <defaultValue>g</defaultValue> </leafNode> <leafNode name="physical-device"> <properties> @@ -637,7 +639,7 @@ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> </valueHelp> <constraint> - <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> </constraint> <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage> <multi/> @@ -670,7 +672,7 @@ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> </valueHelp> <constraint> - <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> </constraint> <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage> <multi/> @@ -695,7 +697,7 @@ <description>Allow both WPA and WPA2</description> </valueHelp> <constraint> - <regex>(wpa|wpa2|both)</regex> + <regex>^(wpa|wpa2|both)$</regex> </constraint> <constraintErrorMessage>Unknown WPA mode</constraintErrorMessage> </properties> @@ -762,10 +764,11 @@ <description>Passively monitor all packets on the frequency/channel</description> </valueHelp> <constraint> - <regex>(access-point|station|monitor)</regex> + <regex>^(access-point|station|monitor)$</regex> </constraint> <constraintErrorMessage>Type must be access-point, station or monitor</constraintErrorMessage> </properties> + <defaultValue>monitor</defaultValue> </leafNode> #include <include/vif.xml.i> #include <include/vif-s.xml.i> diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in index 3a2899b57..8f6629d81 100644 --- a/interface-definitions/lldp.xml.in +++ b/interface-definitions/lldp.xml.in @@ -21,6 +21,7 @@ </valueHelp> <completionHelp> <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + <list>all</list> </completionHelp> </properties> <children> @@ -73,7 +74,7 @@ </completionHelp> <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage> <constraint> - <regex>(WGS84|NAD83|MLLW)$</regex> + <regex>^(WGS84|NAD83|MLLW)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index f8415b7c0..8a14f4d25 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -61,13 +61,13 @@ <properties> <help>NPTv6 rule number</help> <valueHelp> - <format>1-9999</format> + <format>1-999999</format> <description>Number for this rule</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-9999"/> + <validator name="numeric" argument="--range 1-999999"/> </constraint> - <constraintErrorMessage>NAT rule number must be between 1 and 9999</constraintErrorMessage> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> <leafNode name="description"> diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in index 62e2c87b9..8900e7955 100644 --- a/interface-definitions/protocols-bfd.xml.in +++ b/interface-definitions/protocols-bfd.xml.in @@ -28,7 +28,7 @@ <children> <node name="source"> <properties> - <help>Bind listener to specifid interface/address, mandatory for IPv6</help> + <help>Bind listener to specified interface/address, mandatory for IPv6</help> </properties> <children> <leafNode name="interface"> diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in index 376323855..3e9edbf72 100644 --- a/interface-definitions/protocols-mpls.xml.in +++ b/interface-definitions/protocols-mpls.xml.in @@ -54,6 +54,30 @@ </valueHelp> </properties> <children> + <leafNode name="hello-holdtime"> + <properties> + <help>Hello holdtime</help> + <valueHelp> + <format>1-65535</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-interval"> + <properties> + <help>Hello interval</help> + <valueHelp> + <format>1-65535</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> <leafNode name="transport-ipv4-address"> <properties> <help>Transport ipv4 address</help> @@ -95,4 +119,4 @@ </node> </children> </node> -</interfaceDefinition>
\ No newline at end of file +</interfaceDefinition> diff --git a/interface-definitions/mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in index a59321294..e21b1b27c 100644 --- a/interface-definitions/mdns-repeater.xml.in +++ b/interface-definitions/service_mdns-repeater.xml.in @@ -1,5 +1,4 @@ <?xml version="1.0"?> -<!-- mDNS repeater configuration --> <interfaceDefinition> <node name="service"> <children> @@ -8,7 +7,7 @@ <help>Multicast DNS (mDNS) parameters</help> </properties> <children> - <node name="repeater" owner="${vyos_conf_scripts_dir}/mdns_repeater.py"> + <node name="repeater" owner="${vyos_conf_scripts_dir}/service_mdns-repeater.py"> <properties> <help>mDNS repeater configuration</help> <priority>990</priority> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index c7ba2617a..605f47b37 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -311,31 +311,29 @@ <valueless /> </properties> </leafNode> - <node name="mppe"> + <leafNode name="mppe"> <properties> <help>Specifies MPPE negotiation preference. (default prefer mppe)</help> + <completionHelp> + <list>deny prefer require</list> + </completionHelp> + <valueHelp> + <format>deny</format> + <description>Deny MPPE</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for MPPE - do not fail on reject</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Ask client for MPPE - drop connection on reject</description> + </valueHelp> + <constraint> + <regex>^(deny|prefer|require)$</regex> + </constraint> </properties> - <children> - <leafNode name="require"> - <properties> - <help>Ask client for MPPE, if it rejects then drop the connection</help> - <valueless /> - </properties> - </leafNode> - <leafNode name="prefer"> - <properties> - <help>Ask client for MPPE, if it rejects do not fail</help> - <valueless /> - </properties> - </leafNode> - <leafNode name="deny"> - <properties> - <help>Deny MPPE</help> - <valueless /> - </properties> - </leafNode> - </children> - </node> + </leafNode> <leafNode name="lcp-echo-interval"> <properties> <help>LCP echo-requests/sec</help> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index 6a4706ab7..5a472fc9a 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -32,6 +32,7 @@ </constraint> <constraintErrorMessage>Hop count must be between 0 and 255</constraintErrorMessage> </properties> + <defaultValue>64</defaultValue> </leafNode> <leafNode name="default-lifetime"> <properties> @@ -69,10 +70,11 @@ <description>Default router has high preference</description> </valueHelp> <constraint> - <regex>(low|medium|high)</regex> + <regex>^(low|medium|high)$</regex> </constraint> <constraintErrorMessage>Default preference must be low, medium or high</constraintErrorMessage> </properties> + <defaultValue>medium</defaultValue> </leafNode> <leafNode name="dnssl"> <properties> @@ -116,6 +118,7 @@ </constraint> <constraintErrorMessage>Maximum interval must be between 4 and 1800 seconds</constraintErrorMessage> </properties> + <defaultValue>600</defaultValue> </leafNode> <leafNode name="min"> <properties> @@ -191,9 +194,10 @@ </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> - <regex>(infinity)</regex> + <regex>^(infinity)$</regex> </constraint> </properties> + <defaultValue>14400</defaultValue> </leafNode> <leafNode name="valid-lifetime"> <properties> @@ -214,6 +218,7 @@ <regex>(infinity)</regex> </constraint> </properties> + <defaultValue>2592000</defaultValue> </leafNode> </children> </tagNode> @@ -233,6 +238,7 @@ </constraint> <constraintErrorMessage>Reachable time must be 0 or between 1 and 3600000 milliseconds</constraintErrorMessage> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="retrans-timer"> <properties> @@ -250,6 +256,7 @@ </constraint> <constraintErrorMessage>Retransmit interval must be 0 or between 1 and 4294967295 milliseconds</constraintErrorMessage> </properties> + <defaultValue>0</defaultValue> </leafNode> <leafNode name="no-send-advert"> <properties> diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in new file mode 100644 index 000000000..36116ae1b --- /dev/null +++ b/interface-definitions/system-lcd.xml.in @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="lcd" owner="${vyos_conf_scripts_dir}/system_lcd.py"> + <properties> + <help>System LCD display</help> + <priority>100</priority> + </properties> + <children> + <leafNode name="model"> + <properties> + <help>Model of the display attached to this system [REQUIRED]</help> + <completionHelp> + <list>cfa-533 cfa-631 cfa-633 cfa-635 sdec</list> + </completionHelp> + <valueHelp> + <format>cfa-533</format> + <description>Crystalfontz CFA-533</description> + </valueHelp> + <valueHelp> + <format>cfa-631</format> + <description>Crystalfontz CFA-631</description> + </valueHelp> + <valueHelp> + <format>cfa-633</format> + <description>Crystalfontz CFA-633</description> + </valueHelp> + <valueHelp> + <format>cfa-635</format> + <description>Crystalfontz CFA-635</description> + </valueHelp> + <valueHelp> + <format>sdec</format> + <description>Lanner, Watchguard, Nexcom NSA, Sophos UTM appliances</description> + </valueHelp> + <constraint> + <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|sdec)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="device"> + <properties> + <help>Physical device used by LCD display</help> + <completionHelp> + <script>ls -1 /dev | grep ttyS</script> + <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script> + </completionHelp> + <valueHelp> + <format>ttySXX</format> + <description>TTY device name, regular serial port</description> + </valueHelp> + <valueHelp> + <format>usbNbXpY</format> + <description>TTY device name, USB based</description> + </valueHelp> + <constraint> + <regex>^(ttyS[0-9]+|usb[0-9]+b.*)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-options.xml.in b/interface-definitions/system-options.xml.in index 194773329..a5fec10db 100644 --- a/interface-definitions/system-options.xml.in +++ b/interface-definitions/system-options.xml.in @@ -46,13 +46,21 @@ </leafNode> <node name="http-client"> <properties> - <help>Global options used for HTTP based commands</help> + <help>Global options used for HTTP client</help> </properties> <children> #include <include/source-interface.xml.i> #include <include/source-address-ipv4-ipv6.xml.i> </children> </node> + <node name="ssh-client"> + <properties> + <help>Global options used for SSH client</help> + </properties> + <children> + #include <include/source-address-ipv4-ipv6.xml.i> + </children> + </node> </children> </node> </children> diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in new file mode 100644 index 000000000..16fe660a9 --- /dev/null +++ b/interface-definitions/vpn_openconnect.xml.in @@ -0,0 +1,258 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="openconnect" owner="${vyos_conf_scripts_dir}/vpn_openconnect.py"> + <properties> + <help>SSL VPN OpenConnect, AnyConnect compatible server</help> + <priority>901</priority> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication for remote access SSL VPN Server</help> + </properties> + <children> + <leafNode name="mode"> + <properties> + <help>Authentication mode used by this server</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use RADIUS server for user autentication</description> + </valueHelp> + <constraint> + <regex>(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> + </leafNode> + <node name="local-users"> + <properties> + <help>Local user authentication for SSL VPN server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a SSL VPN Server user</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + #include <include/radius-server.xml.i> + <node name="radius"> + <children> + <leafNode name="timeout"> + <properties> + <help>Session timeout</help> + <valueHelp> + <format>1-30</format> + <description>Session timeout in seconds (default: 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-30"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> + </properties> + <defaultValue>2</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + <node name="listen-ports"> + <properties> + <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + </properties> + <children> + <leafNode name="tcp"> + <properties> + <help>tcp port number to accept connections (default: 443)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 443)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>443</defaultValue> + </leafNode> + <leafNode name="udp"> + <properties> + <help>udp port number to accept connections (default: 443)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 443)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>443</defaultValue> + </leafNode> + </children> + </node> + <node name="ssl"> + <properties> + <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + </properties> + <children> + <leafNode name="ca-cert-file"> + <properties> + <help>Certificate Authority certificate</help> + <completionHelp> + <script>ls /config/auth</script> + </completionHelp> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + <leafNode name="cert-file"> + <properties> + <help>Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key-file"> + <properties> + <help>Privat Key of the Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="network-settings"> + <properties> + <help>Network settings</help> + </properties> + <children> + <leafNode name="push-route"> + <properties> + <help>Route to be pushed to the client</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="client-ip-settings"> + <properties> + <help>Client IP pools settings</help> + </properties> + <children> + <leafNode name="subnet"> + <properties> + <help>Client IP subnet (CIDR notation)</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Pool of addresses used to assign to clients</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mask"> + <properties> + <help>Prefix length used for individual client</help> + <valueHelp> + <format><48-128></format> + <description>Client prefix length (default: 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 48-128"/> + </constraint> + </properties> + <defaultValue>64</defaultValue> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> +</interfaceDefinition> diff --git a/op-mode-definitions/add-system-image.xml b/op-mode-definitions/add-system-image.xml new file mode 100644 index 000000000..3dc1c67ab --- /dev/null +++ b/op-mode-definitions/add-system-image.xml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="add"> + <children> + <node name="system"> + <properties> + <help>Add item to a system facility</help> + </properties> + <children> + <tagNode name="image"> + <properties> + <help>Add a new image to the system</help> + <completionHelp> + <list>/path/to/vyos-image.iso http://example.com/vyos-image.iso</list> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}"</command> + <children> + <tagNode name="vrf"> + <properties> + <help>Download image via specified VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}"</command> + <children> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --username "${6}" --password "${8}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/configure.xml b/op-mode-definitions/configure.xml new file mode 100644 index 000000000..3dd5a0f45 --- /dev/null +++ b/op-mode-definitions/configure.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="configure"> + <properties> + <help>Enter configuration mode</help> + </properties> + <command>if [ `id -u` == 0 ]; then + echo "You are attempting to enter configuration mode as root." + echo "It may have unintended consequences and render your system" + echo "unusable until restart." + echo "Please do it as an administrator level VyOS user instead." + else + if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then + echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed" + fi + history -w + export _OFR_CONFIGURE=ok + newgrp vyattacfg + unset _OFR_CONFIGURE + _vyatta_op_do_key_bindings + history -r + fi</command> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/connect-disconnect.xml b/op-mode-definitions/connect.xml index f0fcef5da..1ec62949a 100644 --- a/op-mode-definitions/connect-disconnect.xml +++ b/op-mode-definitions/connect.xml @@ -2,35 +2,27 @@ <interfaceDefinition> <node name="connect"> <properties> - <help>Establish a connection</help> + <help>Establish connection</help> </properties> <children> - <tagNode name="interface"> + <tagNode name="console"> <properties> - <help>Bring up a connection-oriented network interface</help> + <help>Connect to device attached to serial console server</help> <completionHelp> - <path>interfaces pppoe</path> - <path>interfaces wirelessmodem</path> + <path>service console-server device</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> + <command>/usr/bin/console "$3"</command> </tagNode> - </children> - </node> - <node name="disconnect"> - <properties> - <help>Take down a connection</help> - </properties> - <children> <tagNode name="interface"> <properties> - <help>Take down a connection-oriented network interface</help> + <help>Bring up a connection-oriented network interface</help> <completionHelp> <path>interfaces pppoe</path> <path>interfaces wirelessmodem</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/disconnect.xml b/op-mode-definitions/disconnect.xml new file mode 100644 index 000000000..bf2c37b89 --- /dev/null +++ b/op-mode-definitions/disconnect.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="disconnect"> + <properties> + <help>Take down a connection</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Take down a connection-oriented network interface</help> + <completionHelp> + <path>interfaces pppoe</path> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml index 3eadabf0a..c7bcad413 100644 --- a/op-mode-definitions/force-arp.xml +++ b/op-mode-definitions/force-arp.xml @@ -27,13 +27,13 @@ <properties> <help>Send gratuitous ARP reply for specified address</help> </properties> - <command>sudo arping -I $5 -c 1 -A $7</command> + <command>sudo /usr/bin/arping -I $5 -c 1 -A $7</command> <children> <tagNode name="count"> <properties> <help>Send specified number of ARP replies</help> </properties> - <command>sudo arping -I $5 -c $9 -A $7</command> + <command>sudo /usr/bin/arping -I $5 -c $9 -A $7</command> </tagNode> </children> </tagNode> @@ -58,13 +58,13 @@ <properties> <help>Send gratuitous ARP request for specified address</help> </properties> - <command>sudo arping -I $5 -c 1 -U $7</command> + <command>sudo /usr/bin/arping -I $5 -c 1 -U $7</command> <children> <tagNode name="count"> <properties> <help>Send specified number of ARP requests</help> </properties> - <command>sudo arping -I $5 -c $9 -U $7</command> + <command>sudo /usr/bin/arping -I $5 -c $9 -U $7</command> </tagNode> </children> </tagNode> diff --git a/op-mode-definitions/force-ipv6-nd.xml b/op-mode-definitions/force-ipv6-nd.xml new file mode 100644 index 000000000..49de097f6 --- /dev/null +++ b/op-mode-definitions/force-ipv6-nd.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-nd"> + <properties> + <help>IPv6 Neighbor Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Neighbor Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of node to lookup</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/ndisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-ipv6-rd.xml b/op-mode-definitions/force-ipv6-rd.xml new file mode 100644 index 000000000..8c901af25 --- /dev/null +++ b/op-mode-definitions/force-ipv6-rd.xml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-rd"> + <properties> + <help>IPv6 Router Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Router Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 "$4"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of target</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/lldp.xml b/op-mode-definitions/lldp.xml index 105bfe237..297ccf1f4 100644 --- a/op-mode-definitions/lldp.xml +++ b/op-mode-definitions/lldp.xml @@ -17,7 +17,7 @@ <properties> <help>Show LLDP neighbor details</help> </properties> - <command>/usr/sbin/lldpctl -f plain</command> + <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command> </node> <tagNode name="interface"> <properties> @@ -26,7 +26,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/lldp_op.py --interface $4</command> + <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml index d1e459b17..d1e459b17 100644 --- a/op-mode-definitions/bandwidth-test.xml +++ b/op-mode-definitions/monitor-bandwidth-test.xml diff --git a/op-mode-definitions/bandwidth-monitor.xml b/op-mode-definitions/monitor-bandwidth.xml index 9af0a9e70..9af0a9e70 100644 --- a/op-mode-definitions/bandwidth-monitor.xml +++ b/op-mode-definitions/monitor-bandwidth.xml diff --git a/op-mode-definitions/monitor-log.xml b/op-mode-definitions/monitor-log.xml new file mode 100644 index 000000000..99efe5306 --- /dev/null +++ b/op-mode-definitions/monitor-log.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <properties> + <help>Monitor last lines of messages file</help> + </properties> + <command>tail --follow=name /var/log/messages</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-ndp.xml b/op-mode-definitions/monitor-ndp.xml new file mode 100644 index 000000000..1ac6ce39b --- /dev/null +++ b/op-mode-definitions/monitor-ndp.xml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="ndp"> + <properties> + <help>Monitor the NDP information received by the router through the device</help> + </properties> + <command>sudo ndptool monitor</command> + <children> + <tagNode name="interface"> + <command>sudo ndptool monitor --ifname=$4</command> + <properties> + <help>Monitor ndp protocol on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="type"> + <command>sudo ndptool monitor --ifname=$4 --msg-type=$6</command> + <properties> + <help>Monitor specific types of NDP protocols</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </tagNode> + <tagNode name="type"> + <command>sudo ndptool monitor --msg-type=$4</command> + <properties> + <help>Monitor specific types of NDP protocols</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/openconnect.xml b/op-mode-definitions/openconnect.xml new file mode 100644 index 000000000..9b82b114e --- /dev/null +++ b/op-mode-definitions/openconnect.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="openconnect-server"> + <properties> + <help>show openconnect-server information</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active openconnect server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/openconnect-control.py --action="show_sessions"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ping.xml b/op-mode-definitions/ping.xml new file mode 100644 index 000000000..4c25a59ab --- /dev/null +++ b/op-mode-definitions/ping.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <tagNode name="ping"> + <properties> + <help>Send Internet Control Message Protocol (ICMP) echo request</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>Ping options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/ping.py --get-options "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + </leafNode> + </children> + </tagNode> +</interfaceDefinition> diff --git a/op-mode-definitions/show-console-server.xml b/op-mode-definitions/show-console-server.xml index e47b6cfaa..77a7f3376 100644 --- a/op-mode-definitions/show-console-server.xml +++ b/op-mode-definitions/show-console-server.xml @@ -1,18 +1,5 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="connect"> - <children> - <tagNode name="console"> - <properties> - <help>Connect to device attached to serial console server</help> - <completionHelp> - <path>service console-server device</path> - </completionHelp> - </properties> - <command>/usr/bin/console "$3"</command> - </tagNode> - </children> - </node> <node name="show"> <children> <node name="log"> diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml index a49036397..c3ff3a60f 100644 --- a/op-mode-definitions/show-hardware.xml +++ b/op-mode-definitions/show-hardware.xml @@ -71,14 +71,20 @@ <properties> <help>Show peripherals connected to the USB bus</help> </properties> - <command>lsusb</command> + <command>/usr/bin/lsusb -t</command> <children> <node name="detail"> <properties> <help>Show detailed USB bus information</help> </properties> - <command>lsusb -v</command> + <command>/usr/bin/lsusb -v</command> </node> + <leafNode name="serial"> + <properties> + <help>Show information about connected USB serial ports</help> + </properties> + <command>${vyos_op_scripts_dir}/show_usb_serial.py</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-ethernet.xml b/op-mode-definitions/show-interfaces-ethernet.xml index 80f07c2bd..bdcfa55f1 100644 --- a/op-mode-definitions/show-interfaces-ethernet.xml +++ b/op-mode-definitions/show-interfaces-ethernet.xml @@ -74,6 +74,7 @@ <properties> <help>Show ethernet interface information</help> </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show-brief</command> <children> <leafNode name="detail"> <properties> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml index 01acd4fc6..393ca912f 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -26,10 +26,24 @@ <path>interfaces pppoe</path> </completionHelp> </properties> - <command>/usr/sbin/pppstats $4</command> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> </leafNode> </children> </tagNode> + <node name="pppoe"> + <properties> + <help>Show PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml index 1f710b3dc..c0ab9c66f 100644 --- a/op-mode-definitions/show-interfaces-wirelessmodem.xml +++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml @@ -26,10 +26,24 @@ <path>interfaces wirelessmodem</path> </completionHelp> </properties> - <command>/usr/sbin/pppstats $4</command> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> </leafNode> </children> </tagNode> + <node name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Wireless Modem (WWAN( interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-ip-route.xml b/op-mode-definitions/show-ip-route.xml index d12d132c0..48ebbc74a 100644 --- a/op-mode-definitions/show-ip-route.xml +++ b/op-mode-definitions/show-ip-route.xml @@ -149,7 +149,7 @@ <properties> <help>Show longer prefixes of routes for specified IP address or prefix</help> </properties> - <command>/usr/bin/vtysh -c "show ip route $4"</command> + <command>/usr/bin/vtysh -c "show ip route $4 longer-prefixes"</command> </leafNode> </children> </tagNode> diff --git a/op-mode-definitions/show-ipv6-bgp.xml b/op-mode-definitions/show-ipv6-bgp.xml new file mode 100644 index 000000000..67a8c8658 --- /dev/null +++ b/op-mode-definitions/show-ipv6-bgp.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ipv6"> + <children> + <node name="bgp"> + <children> + <tagNode name="route-map"> + <properties> + <help>Show BGP routes matching the specified route map</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp ipv6 route-map $5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-system.xml b/op-mode-definitions/show-system.xml index 74b34ae92..0623e3b62 100644 --- a/op-mode-definitions/show-system.xml +++ b/op-mode-definitions/show-system.xml @@ -59,7 +59,7 @@ <properties> <help>Checks overall system integrity</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command> + <command>sudo ${vyos_op_scripts_dir}/show_system_integrity.py</command> </leafNode> <leafNode name="kernel-messages"> <properties> @@ -176,20 +176,6 @@ </properties> <command>uptime</command> </leafNode> - <node name="usb"> - <properties> - <help>Show information about Universal Serial Bus (USB)</help> - </properties> - <command>/usr/bin/lsusb -t</command> - <children> - <leafNode name="serial"> - <properties> - <help>Show information about connected USB serial ports</help> - </properties> - <command>${vyos_op_scripts_dir}/show_usb_serial.py</command> - </leafNode> - </children> - </node> </children> </node> </children> diff --git a/op-mode-definitions/show-version.xml b/op-mode-definitions/show-version.xml index aae5bb008..905a4865c 100644 --- a/op-mode-definitions/show-version.xml +++ b/op-mode-definitions/show-version.xml @@ -18,7 +18,7 @@ <properties> <help>Show system version and versions of all packages</help> </properties> - <command>${vyos_op_scripts_dir}/show_version.py --all</command> + <command>echo "Package versions:"; dpkg -l | awk '$0~/>/{exit}1'</command> </leafNode> <leafNode name="quagga"> <properties> diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml index 1c806908b..438e7c334 100644 --- a/op-mode-definitions/show-vrf.xml +++ b/op-mode-definitions/show-vrf.xml @@ -11,9 +11,9 @@ <tagNode name="vrf"> <properties> <help>Show information on specific VRF instance</help> - <completionHelp> - <path>vrf name</path> - </completionHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> </properties> <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> <children> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml index 340d28280..1b619ed43 100644 --- a/op-mode-definitions/traceroute.xml +++ b/op-mode-definitions/traceroute.xml @@ -25,6 +25,25 @@ </completionHelp> </properties> <command>/usr/bin/traceroute -4 "$3"</command> + <children> + <node name="tcp"> + <properties> + <help>Route tracing and port detection using TCP</help> + </properties> + <command>sudo /usr/bin/tcptraceroute "$3" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/bin/tcptraceroute "$3" $6</command> + </tagNode> + </children> + </node> + </children> </tagNode> <tagNode name="ipv6"> <properties> @@ -34,6 +53,25 @@ </completionHelp> </properties> <command>/usr/bin/traceroute -6 "$3"</command> + <children> + <node name="tcp"> + <properties> + <help>Use TCP/IPv6 packets to perform a traceroute</help> + </properties> + <command>sudo /usr/bin/tcptraceroute6 "$3" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/bin/tcptraceroute6 "$3" $6</command> + </tagNode> + </children> + </node> + </children> </tagNode> <tagNode name="vrf"> <properties> @@ -62,6 +100,25 @@ </completionHelp> </properties> <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command> + <children> + <node name="tcp"> + <properties> + <help>Route tracing and port detection using TCP</help> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" $8</command> + </tagNode> + </children> + </node> + </children> </tagNode> <tagNode name="ipv6"> <properties> @@ -71,6 +128,25 @@ </completionHelp> </properties> <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command> + <children> + <node name="tcp"> + <properties> + <help>Use TCP/IPv6 packets to perform a traceroute</help> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" $8</command> + </tagNode> + </children> + </node> + </children> </tagNode> </children> </tagNode> @@ -89,6 +165,24 @@ </tagNode> <node name="traceroute"> <children> + <tagNode name="ipv4"> + <properties> + <help>IPv4 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr -4 "$4"</command> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>IPv6 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr -6 "$4"</command> + </tagNode> <tagNode name="vrf"> <properties> <help>Monitor path to destination in realtime via given VRF</help> @@ -97,8 +191,24 @@ </completionHelp> </properties> <children> - <!-- we need an empty tagNode to pass in a plain fqdn/ip address and - let traceroute decide how to handle this parameter --> + <tagNode name="ipv4"> + <properties> + <help>IPv4 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -4 "$6"</command> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>IPv6 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -6 "$6"</command> + </tagNode> <tagNode name=""> <properties> <help>Track network path to specified node via given VRF</help> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 1795fb820..a7bfa36a3 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -96,6 +96,20 @@ <!-- more commands upon request --> </children> </tagNode> + <node name="wireguard"> + <properties> + <help>Show wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/python/vyos/config.py b/python/vyos/config.py index 780b48a7b..de79a3654 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -63,22 +63,14 @@ In operational mode, all functions return values from the running config. """ -import os import re import json -import subprocess +from copy import deepcopy +import vyos.xml import vyos.util import vyos.configtree - -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 - +from vyos.configsource import ConfigSource, ConfigSourceSession class Config(object): """ @@ -88,49 +80,18 @@ class Config(object): the only state it keeps is relative *config path* for convenient access to config subtrees. """ - def __init__(self, session_env=None): - self._cli_shell_api = "/bin/cli-shell-api" - self._level = [] - if session_env: - self.__session_env = session_env - else: - self.__session_env = None - - # Running config can be obtained either from op or conf mode, it always succeeds - # once the config system is initialized during boot; - # before initialization, set to empty string - if os.path.isfile('/tmp/vyos-config-status'): - try: - running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - running_config_text = '' - else: - running_config_text = '' - - # Session config ("active") only exists in conf mode. - # In op mode, we'll just use the same running config for both active and session configs. - if self.in_session(): - try: - session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - session_config_text = '' + def __init__(self, session_env=None, config_source=None): + if config_source is None: + self._config_source = ConfigSourceSession(session_env) else: - session_config_text = running_config_text + if not isinstance(config_source, ConfigSource): + raise TypeError("config_source not of type ConfigSource") + self._config_source = config_source - if running_config_text: - self._running_config = vyos.configtree.ConfigTree(running_config_text) - else: - self._running_config = None - - if session_config_text: - self._session_config = vyos.configtree.ConfigTree(session_config_text) - else: - self._session_config = None - - def _make_command(self, op, path): - args = path.split() - cmd = [self._cli_shell_api, op] + args - return cmd + self._level = [] + self._dict_cache = {} + (self._running_config, + self._session_config) = self._config_source.get_configtree_tuple() def _make_path(self, path): # Backwards-compatibility stuff: original implementation used string paths @@ -146,19 +107,6 @@ class Config(object): raise TypeError("Path must be a whitespace-separated string or a list") return (self._level + path) - def _run(self, cmd): - if self.__session_env: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) - else: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - out = p.stdout.read() - p.wait() - p.communicate() - if p.returncode != 0: - raise VyOSError() - else: - return out.decode('ascii') - def set_level(self, path): """ Set the *edit level*, that is, a relative config tree path. @@ -226,22 +174,14 @@ class Config(object): Returns: True if the config session has uncommited changes, False otherwise. """ - try: - self._run(self._make_command('sessionChanged', '')) - return True - except VyOSError: - return False + return self._config_source.session_changed() 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 + return self._config_source.in_session() def show_config(self, path=[], default=None, effective=False): """ @@ -252,72 +192,64 @@ class Config(object): Returns: str: working configuration """ + return self._config_source.show_config(path, default, effective) - # show_config should be independent of CLI edit level. - # Set the CLI edit environment to the top level, and - # restore original on exit. - save_env = self.__session_env - - env_str = self._run(self._make_command('getEditResetEnv', '')) - env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) - root_env = os.environ - for k, v in env_list: - root_env[k] = v - - self.__session_env = root_env + def get_cached_root_dict(self, effective=False): + cached = self._dict_cache.get(effective, {}) + if cached: + return cached - # FIXUP: by default, showConfig will give you a diff - # if there are uncommitted changes. - # The config parser obviously cannot work with diffs, - # so we need to supress diff production using appropriate - # options for getting either running (active) - # or proposed (working) config. if effective: - path = ['--show-active-only'] + path + config = self._running_config else: - path = ['--show-working-only'] + path - - if isinstance(path, list): - path = " ".join(path) - try: - out = self._run(self._make_command('showConfig', path)) - self.__session_env = save_env - return out - except VyOSError: - self.__session_env = save_env - return(default) + config = self._session_config + + if config: + config_dict = json.loads(config.to_json()) + else: + config_dict = {} - def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False): + self._dict_cache[effective] = config_dict + + return config_dict + + def get_config_dict(self, path=[], effective=False, key_mangling=None, + get_first_key=False, no_multi_convert=False): """ Args: path (str list): Configuration tree path, can be empty effective=False: effective or session config key_mangling=None: mangle dict keys according to regex and replacement get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]} + no_multi_convert=False: if convert, return single value of multi node as list Returns: a dict representation of the config under path """ - config_dict = {} + lpath = self._make_path(path) + root_dict = self.get_cached_root_dict(effective) + conf_dict = vyos.util.get_sub_dict(root_dict, lpath, get_first_key) - if effective: - if self._running_config: - config_dict = json.loads((self._running_config).to_json()) - else: - if self._session_config: - config_dict = json.loads((self._session_config).to_json()) + if not key_mangling and no_multi_convert: + return deepcopy(conf_dict) - config_dict = vyos.util.get_sub_dict(config_dict, self._make_path(path), get_first_key) + xmlpath = lpath if get_first_key else lpath[:-1] - if key_mangling: - if not (isinstance(key_mangling, tuple) and \ - (len(key_mangling) == 2) and \ - isinstance(key_mangling[0], str) and \ - isinstance(key_mangling[1], str)): - raise ValueError("key_mangling must be a tuple of two strings") - else: - config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1]) + if not key_mangling: + conf_dict = vyos.xml.multi_to_list(xmlpath, conf_dict) + return conf_dict - return config_dict + if no_multi_convert is False: + conf_dict = vyos.xml.multi_to_list(xmlpath, conf_dict) + + if not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + + conf_dict = vyos.util.mangle_dict_keys(conf_dict, key_mangling[0], key_mangling[1]) + + return conf_dict def is_multi(self, path): """ @@ -330,12 +262,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isMulti', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_multi(path) def is_tag(self, path): """ @@ -348,12 +276,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isTag', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_tag(path) def is_leaf(self, path): """ @@ -366,12 +290,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isLeaf', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_leaf(path) def return_value(self, path, default=None): """ @@ -532,9 +452,6 @@ class Config(object): Returns: str list: child node names - - Raises: - VyOSError: if the node is not a tag node """ if self._running_config: try: diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 0dc7578d8..e8c0aa5b3 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -15,15 +15,13 @@ """ A library for retrieving value dicts from VyOS configs in a declarative fashion. - """ +import os from enum import Enum from copy import deepcopy from vyos import ConfigError -from vyos.validate import is_member -from vyos.util import ifname_from_config def retrieve_config(path_hash, base_path, config): """ @@ -94,7 +92,7 @@ def dict_merge(source, destination): tmp = deepcopy(destination) for key, value in source.items(): - if key not in tmp.keys(): + if key not in tmp: tmp[key] = value elif isinstance(source[key], dict): tmp[key] = dict_merge(source[key], tmp[key]) @@ -102,397 +100,212 @@ def dict_merge(source, destination): return tmp def list_diff(first, second): - """ - Diff two dictionaries and return only unique items - """ + """ Diff two dictionaries and return only unique items """ second = set(second) return [item for item in first if item not in second] - -def get_ethertype(ethertype_val): - if ethertype_val == '0x88A8': - return '802.1ad' - elif ethertype_val == '0x8100': - return '802.1q' - else: - raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) - -dhcpv6_pd_default_data = { - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'dhcpv6_pd_length': '', - 'dhcpv6_pd_interfaces': [] -} - -interface_default_data = { - **dhcpv6_pd_default_data, - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'disable': False, - 'disable_link_detect': 1, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_eui64_prefix_remove': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'mac': '', - 'mtu': 1500, - 'vrf': '' -} - -vlan_default = { - **interface_default_data, - 'egress_qos': '', - 'egress_qos_changed': False, - 'ingress_qos': '', - 'ingress_qos_changed': False, - 'vif_c': {}, - 'vif_c_remove': [] -} - -# see: https://docs.python.org/3/library/enum.html#functional-api -disable = Enum('disable','none was now both') - -def disable_state(conf, check=[3,5,7]): +def T2665_default_dict_cleanup(dict): + """ Cleanup default keys for tag nodes https://phabricator.vyos.net/T2665. """ + # Cleanup + for vif in ['vif', 'vif_s']: + if vif in dict: + for key in ['ip', 'mtu', 'dhcpv6_options']: + if key in dict[vif]: + del dict[vif][key] + + # cleanup VIF-S defaults + if 'vif_c' in dict[vif]: + for key in ['ip', 'mtu', 'dhcpv6_options']: + if key in dict[vif]['vif_c']: + del dict[vif]['vif_c'][key] + # If there is no vif-c defined and we just cleaned the default + # keys - we can clean the entire vif-c dict as it's useless + if not dict[vif]['vif_c']: + del dict[vif]['vif_c'] + + # If there is no real vif/vif-s defined and we just cleaned the default + # keys - we can clean the entire vif dict as it's useless + if not dict[vif]: + del dict[vif] + + if 'dhcpv6_options' in dict and 'pd' in dict['dhcpv6_options']: + if 'length' in dict['dhcpv6_options']['pd']: + del dict['dhcpv6_options']['pd']['length'] + + # delete empty dicts + if 'dhcpv6_options' in dict: + if 'pd' in dict['dhcpv6_options']: + # test if 'pd' is an empty node so we can remove it + if not dict['dhcpv6_options']['pd']: + del dict['dhcpv6_options']['pd'] + + # test if 'dhcpv6_options' is an empty node so we can remove it + if not dict['dhcpv6_options']: + del dict['dhcpv6_options'] + + return dict + +def leaf_node_changed(conf, path): """ - return if and how a particual section of the configuration is has disable'd - using "disable" including if it was disabled by one of its parent. - - check: a list of the level we should check, here 7,5 and 3 - interfaces ethernet eth1 vif-s 1 vif-c 2 disable - interfaces ethernet eth1 vif 1 disable - interfaces ethernet eth1 disable - - it returns an enum (none, was, now, both) + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned """ - - # save where we are in the config - current_level = conf.get_level() - - # logic to figure out if the interface (or one of it parent is disabled) - eff_disable = False - act_disable = False - - levels = check[:] - working_level = current_level[:] - - while levels: - position = len(working_level) - if not position: - break - if position not in levels: - working_level = working_level[:-1] - continue - - levels.remove(position) - conf.set_level(working_level) - working_level = working_level[:-1] - - eff_disable = eff_disable or conf.exists_effective('disable') - act_disable = act_disable or conf.exists('disable') - - conf.set_level(current_level) - - # how the disabling changed - if eff_disable and act_disable: - return disable.both - if eff_disable and not eff_disable: - return disable.was - if not eff_disable and act_disable: - return disable.now - return disable.none - - -def intf_to_dict(conf, default): - from vyos.ifconfig import Interface - + from vyos.configdiff import get_config_diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + (new, old) = D.get_value_diff(path) + if new != old: + if isinstance(old, str): + return old + elif isinstance(old, list): + if isinstance(new, str): + new = [new] + elif isinstance(new, type(None)): + new = [] + return list_diff(old, new) + + return None + +def node_changed(conf, path): """ - Common used function which will extract VLAN related information from config - and represent the result as Python dictionary. - - Function call's itself recursively if a vif-s/vif-c pair is detected. + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned """ + from vyos.configdiff import get_config_diff, Diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys() + return list(keys) + +def get_removed_vlans(conf, dict): + """ + Common function to parse a dictionary retrieved via get_config_dict() and + determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q. + """ + from vyos.configdiff import get_config_diff, Diff - intf = deepcopy(default) - intf['intf'] = ifname_from_config(conf) - - current_vif_list = conf.list_nodes(['vif']) - previous_vif_list = conf.list_effective_nodes(['vif']) - - # set the vif to be deleted - for vif in previous_vif_list: - if vif not in current_vif_list: - intf['vif_remove'].append(vif) - - # retrieve interface description - if conf.exists(['description']): - intf['description'] = conf.return_value(['description']) - - # get DHCP client identifier - if conf.exists(['dhcp-options', 'client-id']): - intf['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id']) - - # DHCP client host name (overrides the system host name) - if conf.exists(['dhcp-options', 'host-name']): - intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name']) - - # DHCP client vendor identifier - if conf.exists(['dhcp-options', 'vendor-class-id']): - intf['dhcp_vendor_class_id'] = conf.return_value( - ['dhcp-options', 'vendor-class-id']) + # Check vif, vif-s/vif-c VLAN interfaces for removal + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_remove': [*keys]}) - # DHCPv6 only acquire config parameters, no address - if conf.exists(['dhcpv6-options', 'parameters-only']): - intf['dhcpv6_prm_only'] = True + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s_remove': [*keys]}) - # DHCPv6 prefix delegation (RFC3633) - current_level = conf.get_level() - if conf.exists(['dhcpv6-options', 'prefix-delegation']): - dhcpv6_pd_path = current_level + ['dhcpv6-options', 'prefix-delegation'] - conf.set_level(dhcpv6_pd_path) + for vif in dict.get('vif_s', {}).keys(): + keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}}) - # retriebe DHCPv6-PD prefix helper length as some ISPs only hand out a - # /64 by default (https://phabricator.vyos.net/T2506) - if conf.exists(['length']): - intf['dhcpv6_pd_length'] = conf.return_value(['length']) + return dict - for interface in conf.list_nodes(['interface']): - conf.set_level(dhcpv6_pd_path + ['interface', interface]) - pd = { - 'ifname': interface, - 'sla_id': '', - 'sla_len': '', - 'if_id': '' - } - if conf.exists(['sla-id']): - pd['sla_id'] = conf.return_value(['sla-id']) +def dict_add_dhcpv6pd_defaults(defaults, config_dict): + # Implant default dictionary for DHCPv6-PD instances + if 'dhcpv6_options' in config_dict and 'pd' in config_dict['dhcpv6_options']: + for pd, pd_config in config_dict['dhcpv6_options']['pd'].items(): + config_dict['dhcpv6_options']['pd'][pd] = dict_merge( + defaults, pd_config) - if conf.exists(['sla-len']): - pd['sla_len'] = conf.return_value(['sla-len']) + return config_dict - if conf.exists(['address']): - pd['if_id'] = conf.return_value(['address']) - - intf['dhcpv6_pd_interfaces'].append(pd) - - # re-set config level - conf.set_level(current_level) - - # DHCPv6 temporary IPv6 address - if conf.exists(['dhcpv6-options', 'temporary']): - intf['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists(['disable-link-detect']): - intf['disable_link_detect'] = 2 - - # ARP filter configuration - if conf.exists(['ip', 'disable-arp-filter']): - intf['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists(['ip', 'enable-arp-accept']): - intf['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists(['ip', 'enable-arp-announce']): - intf['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists(['ip', 'enable-arp-ignore']): - intf['ip_enable_arp_ignore'] = 1 - - # Enable Proxy ARP - if conf.exists(['ip', 'enable-proxy-arp']): - intf['ip_proxy_arp'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists(['ipv6', 'address', 'autoconf']): - intf['ipv6_autoconf'] = 1 - - # Disable IPv6 forwarding on this interface - if conf.exists(['ipv6', 'disable-forwarding']): - intf['ipv6_forwarding'] = 0 - - # check if interface is member of a bridge - intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge') - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists(['ipv6', 'dup-addr-detect-transmits']): - intf['ipv6_dup_addr_detect'] = int( - conf.return_value(['ipv6', 'dup-addr-detect-transmits'])) - - # Media Access Control (MAC) address - if conf.exists(['mac']): - intf['mac'] = conf.return_value(['mac']) - - # Maximum Transmission Unit (MTU) - if conf.exists(['mtu']): - intf['mtu'] = int(conf.return_value(['mtu'])) - - # retrieve VRF instance - if conf.exists(['vrf']): - intf['vrf'] = conf.return_value(['vrf']) - - # egress QoS - if conf.exists(['egress-qos']): - intf['egress_qos'] = conf.return_value(['egress-qos']) - - # egress changes QoS require VLAN interface recreation - if conf.return_effective_value(['egress-qos']): - if intf['egress_qos'] != conf.return_effective_value(['egress-qos']): - intf['egress_qos_changed'] = True - - # ingress QoS - if conf.exists(['ingress-qos']): - intf['ingress_qos'] = conf.return_value(['ingress-qos']) - - # ingress changes QoS require VLAN interface recreation - if conf.return_effective_value(['ingress-qos']): - if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']): - intf['ingress_qos_changed'] = True - - # Get the interface addresses - intf['address'] = conf.return_values(['address']) - - # addresses to remove - difference between effective and working config - intf['address_remove'] = list_diff( - conf.return_effective_values(['address']), intf['address']) - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64']) - - # EUI64 to remove - difference between effective and working config - intf['ipv6_eui64_prefix_remove'] = list_diff( - conf.return_effective_values(['ipv6', 'address', 'eui64']), - intf['ipv6_eui64_prefix']) - - # Determine if the interface should be disabled - disabled = disable_state(conf) - if disabled == disable.both: - # was and is still disabled - intf['disable'] = True - elif disabled == disable.now: - # it is now disable but was not before - intf['disable'] = True - elif disabled == disable.was: - # it was disable but not anymore - intf['disable'] = False - else: - # normal change - intf['disable'] = False - - # Remove the default link-local address if no-default-link-local is set, - # if member of a bridge or if disabled (it may not have a MAC if it's down) - if ( conf.exists(['ipv6', 'address', 'no-default-link-local']) - or intf.get('is_bridge_member') or intf['disable'] ): - intf['ipv6_eui64_prefix_remove'].append('fe80::/64') - else: - # add the link-local by default to make IPv6 work - intf['ipv6_eui64_prefix'].append('fe80::/64') - - # If MAC has changed, remove and re-add all IPv6 EUI64 addresses - try: - interface = Interface(intf['intf'], create=False) - if intf['mac'] and intf['mac'] != interface.get_mac(): - intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix'] - except Exception: - # If the interface does not exist, it could not have changed - pass - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']: - intf['ipv6_accept_ra'] = 2 - - return intf, disable - - - -def add_to_dict(conf, disabled, ifdict, section, key): +def get_interface_dict(config, base, ifname=''): """ - parse a section of vif/vif-s/vif-c and add them to the dict - follow the convention to: - * use the "key" for what to add - * use the "key" what what to remove - - conf: is the Config() already at the level we need to parse - disabled: is a disable enum so we know how to handle to data - intf: if the interface dictionary - section: is the section name to parse (vif/vif-s/vif-c) - key: is the dict key to use (vif/vifs/vifc) + Common utility function to retrieve and mandgle the interfaces available + in CLI configuration. All interfaces have a common base ground where the + value retrival is identical - so it can and should be reused + + Will return a dictionary with the necessary interface configuration """ + from vyos.util import vyos_dict_search + from vyos.validate import is_member + from vyos.xml import defaults + + if not ifname: + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # retrieve interface default values + default_values = defaults(base) + + # setup config level which is extracted in get_removed_vlans() + config.set_level(base + [ifname]) + dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + + # Check if interface has been removed + if dict == {}: + dict.update({'deleted' : ''}) + + # Add interface instance name into dictionary + dict.update({'ifname': ifname}) + + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary + # retrived. + dict = dict_merge(default_values, dict) + + # Check if we are a member of a bridge device + bridge = is_member(config, ifname, 'bridge') + if bridge: + dict.update({'is_bridge_member' : bridge}) + + # Check if we are a member of a bond device + bond = is_member(config, ifname, 'bonding') + if bond: + dict.update({'is_bond_member' : bond}) + + mac = leaf_node_changed(config, ['mac']) + if mac: + dict.update({'mac_old' : mac}) + + eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) + if eui64: + tmp = vyos_dict_search('ipv6.address', dict) + if not tmp: + dict.update({'ipv6': {'address': {'eui64_old': eui64}}}) + else: + dict['ipv6']['address'].update({'eui64_old': eui64}) + + # remove wrongly inserted values + dict = T2665_default_dict_cleanup(dict) + + # Implant default dictionary for DHCPv6-PD instances + default_pd_values = defaults(base + ['dhcpv6-options', 'pd']) + dict = dict_add_dhcpv6pd_defaults(default_pd_values, dict) + + # Implant default dictionary in vif/vif-s VLAN interfaces. Values are + # identical for all types of VLAN interfaces as they all include the same + # XML definitions which hold the defaults. + default_vif_values = defaults(base + ['vif']) + for vif, vif_config in dict.get('vif', {}).items(): + dict['vif'][vif] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_config) + dict['vif'][vif] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_config)) + + for vif_s, vif_s_config in dict.get('vif_s', {}).items(): + dict['vif_s'][vif_s] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_s_config) + dict['vif_s'][vif_s] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_s_config)) + for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): + dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_c_config) + dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_c_config)) + + # Check vif, vif-s/vif-c VLAN interfaces for removal + dict = get_removed_vlans(config, dict) + + return dict - if not conf.exists(section): - return ifdict - - effect = conf.list_effective_nodes(section) - active = conf.list_nodes(section) - - # the section to parse for vlan - sections = [] - - # determine which interfaces to add or remove based on disable state - if disabled == disable.both: - # was and is still disabled - ifdict[f'{key}_remove'] = [] - elif disabled == disable.now: - # it is now disable but was not before - ifdict[f'{key}_remove'] = effect - elif disabled == disable.was: - # it was disable but not anymore - ifdict[f'{key}_remove'] = [] - sections = active - else: - # normal change - # get interfaces (currently effective) - to determine which - # interface is no longer present and needs to be removed - ifdict[f'{key}_remove'] = list_diff(effect, active) - sections = active - - current_level = conf.get_level() - - # add each section, the key must already exists - for s in sections: - # set config level to vif interface - conf.set_level(current_level + [section, s]) - # add the vlan config as a key (vlan id) - value (config) pair - ifdict[key][s] = vlan_to_dict(conf) - - # re-set configuration level to leave things as found - conf.set_level(current_level) - - return ifdict - - -def vlan_to_dict(conf, default=vlan_default): - vlan, disabled = intf_to_dict(conf, default) - - # if this is a not within vif-s node, we are done - if conf.get_level()[-2] != 'vif-s': - return vlan - - # ethertype is mandatory on vif-s nodes and only exists here! - # ethertype uses a default of 0x88A8 - tmp = '0x88A8' - if conf.exists('ethertype'): - tmp = conf.return_value('ethertype') - vlan['ethertype'] = get_ethertype(tmp) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c') - - return vlan diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py new file mode 100644 index 000000000..0e41fbe27 --- /dev/null +++ b/python/vyos/configdiff.py @@ -0,0 +1,249 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from enum import IntFlag, auto + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.xml import defaults + +class ConfigDiffError(Exception): + """ + Raised on config dict access errors, for example, calling get_value on + a non-leaf node. + """ + pass + +def enum_to_key(e): + return e.name.lower() + +class Diff(IntFlag): + MERGE = auto() + DELETE = auto() + ADD = auto() + STABLE = auto() + +requires_effective = [enum_to_key(Diff.DELETE)] +target_defaults = [enum_to_key(Diff.MERGE)] + +def _key_sets_from_dicts(session_dict, effective_dict): + session_keys = list(session_dict) + effective_keys = list(effective_dict) + + ret = {} + stable_keys = [k for k in session_keys if k in effective_keys] + + ret[enum_to_key(Diff.MERGE)] = session_keys + ret[enum_to_key(Diff.DELETE)] = [k for k in effective_keys if k not in stable_keys] + ret[enum_to_key(Diff.ADD)] = [k for k in session_keys if k not in stable_keys] + ret[enum_to_key(Diff.STABLE)] = stable_keys + + return ret + +def _dict_from_key_set(key_set, d): + # This will always be applied to a key_set obtained from a get_sub_dict, + # hence there is no possibility of KeyError, as get_sub_dict guarantees + # a return type of dict + ret = {k: d[k] for k in key_set} + + return ret + +def get_config_diff(config, key_mangling=None): + """ + Check type and return ConfigDiff instance. + """ + if not config or not isinstance(config, Config): + raise TypeError("argument must me a Config instance") + if key_mangling and not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + + return ConfigDiff(config, key_mangling) + +class ConfigDiff(object): + """ + The class of config changes as represented by comparison between the + session config dict and the effective config dict. + """ + def __init__(self, config, key_mangling=None): + self._level = config.get_level() + self._session_config_dict = config.get_cached_root_dict(effective=False) + self._effective_config_dict = config.get_cached_root_dict(effective=True) + self._key_mangling = key_mangling + + # mirrored from Config; allow path arguments relative to level + def _make_path(self, path): + if isinstance(path, str): + path = path.split() + elif isinstance(path, list): + pass + else: + raise TypeError("Path must be a whitespace-separated string or a list") + + ret = self._level + path + return ret + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config dict path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``get_value("name-server")`` is equivalent to calling + ``get_value("system name-server")`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + if isinstance(path, str): + if path: + self._level = path.split() + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + ret = self._level.copy() + return ret + + def _mangle_dict_keys(self, config_dict): + config_dict = mangle_dict_keys(config_dict, self._key_mangling[0], + self._key_mangling[1]) + return config_dict + + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, under path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, + self._make_path(path), get_first_key=True) + effective_dict = get_sub_dict(self._effective_config_dict, + self._make_path(path), get_first_key=True) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, at path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_value_diff(self, path=[]): + """ + Args: + path (str|list): config path + + Returns: (new, old) tuple of values in session config/effective config + """ + # one should properly use is_leaf as check; for the moment we will + # deduce from type, which will not catch call on non-leaf node if None + new_value_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + old_value_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + new_value = None + old_value = None + if new_value_dict: + new_value = next(iter(new_value_dict.values())) + if old_value_dict: + old_value = next(iter(old_value_dict.values())) + + if new_value and isinstance(new_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + if old_value and isinstance(old_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + + return new_value, old_value diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index f2524b37e..6e4214360 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -17,6 +17,8 @@ import re import sys import subprocess +from vyos.util import call + CLI_SHELL_API = '/bin/cli-shell-api' SET = '/opt/vyatta/sbin/my_set' DELETE = '/opt/vyatta/sbin/my_delete' @@ -26,7 +28,7 @@ DISCARD = '/opt/vyatta/sbin/my_discard' SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl'] -INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image'] +INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url'] REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] @@ -69,6 +71,11 @@ def inject_vyos_env(env): env['vyos_sbin_dir'] = '/usr/sbin' env['vyos_validators_dir'] = '/usr/libexec/vyos/validators' + # if running the vyos-configd daemon, inject the vyshim env var + ret = call('systemctl is-active --quiet vyos-configd.service') + if not ret: + env['vyshim'] = '/usr/sbin/vyshim' + return env diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py new file mode 100644 index 000000000..50222e385 --- /dev/null +++ b/python/vyos/configsource.py @@ -0,0 +1,318 @@ + +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import subprocess + +from vyos.configtree import ConfigTree + +class VyOSError(Exception): + """ + Raised on config access errors. + """ + pass + +class ConfigSourceError(Exception): + ''' + Raised on error in ConfigSource subclass init. + ''' + pass + +class ConfigSource: + def __init__(self): + self._running_config: ConfigTree = None + self._session_config: ConfigTree = None + + def get_configtree_tuple(self): + return self._running_config, self._session_config + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + +class ConfigSourceSession(ConfigSource): + def __init__(self, session_env=None): + super().__init__() + self._cli_shell_api = "/bin/cli-shell-api" + self._level = [] + if session_env: + self.__session_env = session_env + else: + self.__session_env = None + + # Running config can be obtained either from op or conf mode, it always succeeds + # once the config system is initialized during boot; + # before initialization, set to empty string + if os.path.isfile('/tmp/vyos-config-status'): + try: + running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + running_config_text = '' + else: + running_config_text = '' + + # Session config ("active") only exists in conf mode. + # In op mode, we'll just use the same running config for both active and session configs. + if self.in_session(): + try: + session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + session_config_text = '' + else: + session_config_text = running_config_text + + if running_config_text: + self._running_config = ConfigTree(running_config_text) + else: + self._running_config = None + + if session_config_text: + self._session_config = ConfigTree(session_config_text) + else: + self._session_config = None + + def _make_command(self, op, path): + args = path.split() + cmd = [self._cli_shell_api, op] + args + return cmd + + def _run(self, cmd): + if self.__session_env: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) + else: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + out = p.stdout.read() + p.wait() + p.communicate() + if p.returncode != 0: + raise VyOSError() + else: + return out.decode('ascii') + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config tree path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``exists("name-server")`` is equivalent to calling + ``exists("system name-server"`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + # Make sure there's always a space between default path (level) + # and path supplied as method argument + # XXX: for small strings in-place concatenation is not a problem + if isinstance(path, str): + if path: + self._level = re.split(r'\s+', path) + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + try: + self._run(self._make_command('sessionChanged', '')) + return True + except VyOSError: + return False + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + try: + self._run(self._make_command('inSession', '')) + return True + except VyOSError: + return False + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + + # FIXUP: by default, showConfig will give you a diff + # if there are uncommitted changes. + # The config parser obviously cannot work with diffs, + # so we need to supress diff production using appropriate + # options for getting either running (active) + # or proposed (working) config. + if effective: + path = ['--show-active-only'] + path + else: + path = ['--show-working-only'] + path + + if isinstance(path, list): + path = " ".join(path) + try: + out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env + return out + except VyOSError: + self.__session_env = save_env + return(default) + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isMulti', path)) + return True + except VyOSError: + return False + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isTag', path)) + return True + except VyOSError: + return False + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isLeaf', path)) + return True + except VyOSError: + return False + +class ConfigSourceString(ConfigSource): + def __init__(self, running_config_text=None, session_config_text=None): + super().__init__() + + try: + self._running_config = ConfigTree(running_config_text) if running_config_text else None + self._session_config = ConfigTree(session_config_text) if session_config_text else None + except ValueError: + raise ConfigSourceError(f"Init error in {type(self)}") diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 32129a048..7e1930878 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -29,11 +29,11 @@ def verify_vrf(config): recurring validation of VRF configuration. """ from netifaces import interfaces - if 'vrf' in config.keys(): + if 'vrf' in config: if config['vrf'] not in interfaces(): raise ConfigError('VRF "{vrf}" does not exist'.format(**config)) - if 'is_bridge_member' in config.keys(): + if 'is_bridge_member' in config: raise ConfigError( 'Interface "{ifname}" cannot be both a member of VRF "{vrf}" ' 'and bridge "{is_bridge_member}"!'.format(**config)) @@ -41,14 +41,14 @@ def verify_vrf(config): def verify_address(config): """ - Common helper function used by interface implementations to - perform recurring validation of IP address assignmenr - when interface also is part of a bridge. + Common helper function used by interface implementations to perform + recurring validation of IP address assignment when interface is part + of a bridge or bond. """ if {'is_bridge_member', 'address'} <= set(config): raise ConfigError( - f'Cannot assign address to interface "{ifname}" as it is a ' - f'member of bridge "{is_bridge_member}"!'.format(**config)) + 'Cannot assign address to interface "{ifname}" as it is a ' + 'member of bridge "{is_bridge_member}"!'.format(**config)) def verify_bridge_delete(config): @@ -57,11 +57,20 @@ def verify_bridge_delete(config): perform recurring validation of IP address assignmenr when interface also is part of a bridge. """ - if 'is_bridge_member' in config.keys(): + if 'is_bridge_member' in config: raise ConfigError( 'Interface "{ifname}" cannot be deleted as it is a ' 'member of bridge "{is_bridge_member}"!'.format(**config)) +def verify_interface_exists(config): + """ + Common helper function used by interface implementations to perform + recurring validation if an interface actually exists. + """ + from netifaces import interfaces + if not config['ifname'] in interfaces(): + raise ConfigError('Interface "{ifname}" does not exist!' + .format(**config)) def verify_source_interface(config): """ @@ -70,9 +79,61 @@ def verify_source_interface(config): required by e.g. peth/MACvlan, MACsec ... """ from netifaces import interfaces - if not 'source_interface' in config.keys(): + if 'source_interface' not in config: raise ConfigError('Physical source-interface required for ' 'interface "{ifname}"'.format(**config)) - if not config['source_interface'] in interfaces(): - raise ConfigError(f'Source interface {source_interface} does not ' - f'exist'.format(**config)) + if config['source_interface'] not in interfaces(): + raise ConfigError('Source interface {source_interface} does not ' + 'exist'.format(**config)) + +def verify_dhcpv6(config): + """ + Common helper function used by interface implementations to perform + recurring validation of DHCPv6 options which are mutually exclusive. + """ + if 'dhcpv6_options' in config: + from vyos.util import vyos_dict_search + + if {'parameters_only', 'temporary'} <= set(config['dhcpv6_options']): + raise ConfigError('DHCPv6 temporary and parameters-only options ' + 'are mutually exclusive!') + + # It is not allowed to have duplicate SLA-IDs as those identify an + # assigned IPv6 subnet from a delegated prefix + for pd in vyos_dict_search('dhcpv6_options.pd', config): + sla_ids = [] + for interface in vyos_dict_search(f'dhcpv6_options.pd.{pd}.interface', config): + sla_id = vyos_dict_search( + f'dhcpv6_options.pd.{pd}.interface.{interface}.sla_id', config) + sla_ids.append(sla_id) + + # Check for duplicates + duplicates = [x for n, x in enumerate(sla_ids) if x in sla_ids[:n]] + if duplicates: + raise ConfigError('Site-Level Aggregation Identifier (SLA-ID) ' + 'must be unique per prefix-delegation!') + +def verify_vlan_config(config): + """ + Common helper function used by interface implementations to perform + recurring validation of interface VLANs + """ + # 802.1q VLANs + for vlan in config.get('vif', {}): + vlan = config['vif'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + # 802.1ad (Q-in-Q) VLANs + for vlan in config.get('vif_s', {}): + vlan = config['vif_s'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + for vlan in config.get('vif_s', {}).get('vif_c', {}): + vlan = config['vif_c'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 3062ed31c..9921e3b5f 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -21,7 +21,8 @@ directories = { "current": "/opt/vyatta/etc/config-migrate/current", "migrate": "/opt/vyatta/etc/config-migrate/migrate", "log": "/var/log/vyatta", - "templates": "/usr/share/vyos/templates/" + "templates": "/usr/share/vyos/templates/", + "certbot": "/config/auth/letsencrypt" } cfg_group = 'vyattacfg' diff --git a/python/vyos/frr.py b/python/vyos/frr.py index e39b6a914..3fc75bbdf 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -281,7 +281,7 @@ def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line v startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es """ - return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^{before_re}$') + return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^({before_re})$') def remove_section(config, from_re, to_re='!'): diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index a7cdeadd1..9cd8d44c1 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -13,12 +13,10 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. - from vyos.ifconfig.section import Section from vyos.ifconfig.control import Control from vyos.ifconfig.interface import Interface from vyos.ifconfig.operational import Operational -from vyos.ifconfig.dhcp import DHCP from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.bond import BondIf diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 47dd4ff34..64407401b 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -18,10 +18,11 @@ import os from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN +from vyos.util import cmd +from vyos.util import vyos_dict_search from vyos.validate import assert_list from vyos.validate import assert_positive - @Interface.register @VLAN.enable class BondIf(Interface): @@ -179,7 +180,13 @@ class BondIf(Interface): >>> BondIf('bond0').get_arp_ip_target() '192.0.2.1' """ - return self.get_interface('bond_arp_ip_target') + # As this function might also be called from update() of a VLAN interface + # we must check if the bond_arp_ip_target retrieval worked or not - as this + # can not be set for a bond vif interface + try: + return self.get_interface('bond_arp_ip_target') + except FileNotFoundError: + return '' def set_arp_ip_target(self, target): """ @@ -209,11 +216,31 @@ class BondIf(Interface): >>> BondIf('bond0').add_port('eth0') >>> BondIf('bond0').add_port('eth1') """ - # An interface can only be added to a bond if it is in 'down' state. If - # interface is in 'up' state, the following Kernel error will be thrown: - # bond0: eth1 is up - this may be due to an out of date ifenslave. - Interface(interface).set_admin_state('down') - return self.set_interface('bond_add_port', f'+{interface}') + + # From drivers/net/bonding/bond_main.c: + # ... + # bond_set_slave_link_state(new_slave, + # BOND_LINK_UP, + # BOND_SLAVE_NOTIFY_NOW); + # ... + # + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the CLI will tell us! + + # Physical interface must be in admin down state before they can be + # enslaved. If this is not the case an error will be shown: + # bond0: eth0 is up - this may be due to an out of date ifenslave + slave = Interface(interface) + slave_state = slave.get_admin_state() + if slave_state == 'up': + slave.set_admin_state('down') + + ret = self.set_interface('bond_add_port', f'+{interface}') + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the LI is configured for - thus we place the interface in its + # desired state + slave.set_admin_state(slave_state) + return ret def del_port(self, interface): """ @@ -277,3 +304,80 @@ class BondIf(Interface): >>> BondIf('bond0').set_mode('802.3ad') """ return self.set_interface('bond_mode', mode) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # use ref-counting function to place an interface into admin down state. + # set_admin_state_up() must be called the same amount of times else the + # interface won't come up. This can/should be used to prevent link flapping + # when changing interface parameters require the interface to be down. + # We will disable it once before reconfiguration and enable it afterwards. + if 'shutdown_required' in config: + self.set_admin_state('down') + + # call base class first + super().update(config) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = vyos_dict_search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + + # Bonding transmit hash policy + value = config.get('hash_policy') + if value: self.set_hash_policy(value) + + # Some interface options can only be changed if the interface is + # administratively down + if self.get_admin_state() == 'down': + # Delete bond member port(s) + for interface in self.get_slaves(): + self.del_port(interface) + + # Bonding policy/mode + value = config.get('mode') + if value: self.set_mode(value) + + # Add (enslave) interfaces to bond + value = vyos_dict_search('member.interface', config) + if value: + for interface in value: + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + self.add_port(interface) + + # Primary device interface - must be set after 'mode' + value = config.get('primary') + if value: self.set_primary(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 44b92c1db..4c76fe996 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -13,12 +13,12 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. - from vyos.ifconfig.interface import Interface - +from vyos.ifconfig.stp import STP from vyos.validate import assert_boolean from vyos.validate import assert_positive - +from vyos.util import cmd +from vyos.util import vyos_dict_search @Interface.register class BridgeIf(Interface): @@ -187,3 +187,77 @@ class BridgeIf(Interface): >>> BridgeIf('br0').del_port('eth1') """ return self.set_interface('del_port', interface) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Set ageing time + value = config.get('aging') + self.set_ageing_time(value) + + # set bridge forward delay + value = config.get('forwarding_delay') + self.set_forward_delay(value) + + # set hello time + value = config.get('hello_time') + self.set_hello_time(value) + + # set max message age + value = config.get('max_age') + self.set_max_age(value) + + # set bridge priority + value = config.get('priority') + self.set_priority(value) + + # enable/disable spanning tree + value = '1' if 'stp' in config else '0' + self.set_stp(value) + + # enable or disable IGMP querier + tmp = vyos_dict_search('igmp.querier', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_querier(value) + + # remove interface from bridge + tmp = vyos_dict_search('member.interface_remove', config) + if tmp: + for member in tmp: + self.del_port(member) + + STPBridgeIf = STP.enable(BridgeIf) + tmp = vyos_dict_search('member.interface', config) + if tmp: + for interface, interface_config in tmp.items(): + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + # enslave interface port to bridge + self.add_port(interface) + + tmp = STPBridgeIf(interface) + # set bridge port path cost + value = interface_config.get('cost') + tmp.set_path_cost(value) + + # set bridge port path priority + value = interface_config.get('priority') + tmp.set_path_priority(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py deleted file mode 100644 index a8b9a2a87..000000000 --- a/python/vyos/ifconfig/dhcp.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -import os - -from vyos.dicts import FixedDict -from vyos.ifconfig.control import Control -from vyos.template import render - -class _DHCPv4 (Control): - def __init__(self, ifname): - super().__init__() - config_base = r'/var/lib/dhcp/dhclient_' - self.options = FixedDict(**{ - 'ifname': ifname, - 'hostname': '', - 'client_id': '', - 'vendor_class_id': '', - 'conf_file': config_base + f'{ifname}.conf', - 'options_file': config_base + f'{ifname}.options', - 'pid_file': config_base + f'{ifname}.pid', - 'lease_file': config_base + f'{ifname}.leases', - }) - - # replace dhcpv4/v6 with systemd.networkd? - def set(self): - """ - Configure interface as DHCP client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.dhcp.v4.set() - """ - if not self.options['hostname']: - # read configured system hostname. - # maybe change to vyos hostd client ??? - with open('/etc/hostname', 'r') as f: - self.options['hostname'] = f.read().rstrip('\n') - - render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) - render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - - return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) - - def delete(self): - """ - De-configure interface as DHCP clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.dhcp.v4.delete() - """ - if not os.path.isfile(self.options['pid_file']): - self._debug_msg('No DHCP client PID found') - return None - - self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) - - # cleanup old config files - for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): - if os.path.isfile(self.options[name]): - os.remove(self.options[name]) - -class _DHCPv6 (Control): - def __init__(self, ifname): - super().__init__() - self.options = FixedDict(**{ - 'ifname': ifname, - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'dhcpv6_pd_interfaces': [], - 'dhcpv6_pd_length': '' - }) - self._conf_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' - - def set(self): - """ - Configure interface as DHCPv6 client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.dhcp.v6.set() - """ - - # better save then sorry .. should be checked in interface script - # but if you missed it we are safe! - if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']: - raise Exception( - 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - - render(self._conf_file, 'dhcp-client/ipv6.tmpl', self.options, trim_blocks=True) - return self._cmd('systemctl restart dhcp6c@{ifname}.service'.format(**self.options)) - - def delete(self): - """ - De-configure interface as DHCPv6 clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.dhcp.v6.delete() - """ - self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options)) - - # cleanup old config files - if os.path.isfile(self._conf_file): - os.remove(self._conf_file) - - -class DHCP(object): - def __init__(self, ifname): - self.v4 = _DHCPv4(ifname) - self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py index 404c490c7..43614cd1c 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -35,3 +35,22 @@ class DummyIf(Interface): 'prefixes': ['dum', ], }, } + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 5b18926c9..17c1bd64d 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -20,7 +20,7 @@ from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN from vyos.validate import assert_list from vyos.util import run - +from vyos.util import vyos_dict_search @Interface.register @VLAN.enable @@ -252,3 +252,58 @@ class EthernetIf(Interface): >>> i.set_udp_offload('on') """ return self.set_interface('ufo', state) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # disable ethernet flow control (pause frames) + value = 'off' if 'disable_flow_control' in config.keys() else 'on' + self.set_flow_control(value) + + # GRO (generic receive offload) + tmp = vyos_dict_search('offload_options.generic_receive', config) + value = tmp if (tmp != None) else 'off' + self.set_gro(value) + + # GSO (generic segmentation offload) + tmp = vyos_dict_search('offload_options.generic_segmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_gso(value) + + # scatter-gather option + tmp = vyos_dict_search('offload_options.scatter_gather', config) + value = tmp if (tmp != None) else 'off' + self.set_sg(value) + + # TSO (TCP segmentation offloading) + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_tso(value) + + # UDP fragmentation offloading + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_ufo(value) + + # Set physical interface speed and duplex + if {'speed', 'duplex'} <= set(config): + speed = config.get('speed') + duplex = config.get('duplex') + self.set_speed_duplex(speed, duplex) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index 145dc268c..dd0658668 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -64,3 +64,22 @@ class GeneveIf(Interface): >> dict = GeneveIf().get_config() """ return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 1819ffc82..ef2336c17 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -16,7 +16,10 @@ import os import re import json +import jmespath + from copy import deepcopy +from glob import glob from ipaddress import IPv4Network from ipaddress import IPv6Address @@ -28,7 +31,10 @@ from netifaces import AF_INET6 from vyos import ConfigError from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.template import render from vyos.util import mac2eui64 +from vyos.util import vyos_dict_search from vyos.validate import is_ipv4 from vyos.validate import is_ipv6 from vyos.validate import is_intf_addr_assigned @@ -40,11 +46,17 @@ from vyos.validate import assert_positive from vyos.validate import assert_range from vyos.ifconfig.control import Control -from vyos.ifconfig.dhcp import DHCP from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational from vyos.ifconfig import Section +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) class Interface(Control): # This is the class which will be used to create @@ -72,8 +84,12 @@ class Interface(Control): _command_get = { 'admin_state': { 'shellcmd': 'ip -json link show dev {ifname}', - 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down', - } + 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', + }, + 'vlan_protocol': { + 'shellcmd': 'ip -json -details link show dev {ifname}', + 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), + }, } _command_set = { @@ -197,11 +213,11 @@ class Interface(Control): # make sure the ifname is the first argument and not from the dict self.config['ifname'] = ifname + self._admin_state_down_cnt = 0 # we must have updated config before initialising the Interface super().__init__(**kargs) self.ifname = ifname - self.dhcp = DHCP(ifname) if not self.exists(ifname): # Any instance of Interface, such as Interface('eth0') @@ -323,6 +339,10 @@ class Interface(Control): self.set_interface('mac', mac) + # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion + if prev_state == 'up': + self.set_admin_state('up') + def set_vrf(self, vrf=''): """ Add/Remove interface from given VRF instance. @@ -539,6 +559,17 @@ class Interface(Control): """ self.set_interface('alias', ifalias) + def get_vlan_protocol(self): + """ + Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0.10').get_vlan_protocol() + '802.1Q' + """ + return self.get_interface('vlan_protocol') + def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' @@ -560,7 +591,24 @@ class Interface(Control): >>> Interface('eth0').get_admin_state() 'down' """ - return self.set_interface('admin_state', state) + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + if self.get_vlan_protocol(): + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + if state == 'up': + self._admin_state_down_cnt -= 1 + if self._admin_state_down_cnt < 1: + return self.set_interface('admin_state', state) + else: + self._admin_state_down_cnt += 1 + return self.set_interface('admin_state', state) def set_proxy_arp(self, enable): """ @@ -663,21 +711,24 @@ class Interface(Control): if addr in self._addr: return False + addr_is_v4 = is_ipv4(addr) + # we can't have both DHCP and static IPv4 addresses assigned for a in self._addr: if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or - ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ): + ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): raise ConfigError(( "Can't configure both static IPv4 and DHCP address " "on the same interface")) # add to interface if addr == 'dhcp': - self.dhcp.v4.set() + self.set_dhcp(True) elif addr == 'dhcpv6': - self.dhcp.v6.set() + self.set_dhcpv6(True) elif not is_intf_addr_assigned(self.ifname, addr): - self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"') + self._cmd(f'ip addr add "{addr}" ' + f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') else: return False @@ -713,9 +764,9 @@ class Interface(Control): # remove from interface if addr == 'dhcp': - self.dhcp.v4.delete() + self.set_dhcp(False) elif addr == 'dhcpv6': - self.dhcp.v6.delete() + self.set_dhcpv6(False) elif is_intf_addr_assigned(self.ifname, addr): self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') else: @@ -734,8 +785,8 @@ class Interface(Control): Will raise an exception on error. """ # stop DHCP(v6) if running - self.dhcp.v4.delete() - self.dhcp.v6.delete() + self.set_dhcp(False) + self.set_dhcpv6(False) # flush all addresses self._cmd(f'ip addr flush dev "{self.ifname}"') @@ -759,23 +810,103 @@ class Interface(Control): return True + def set_dhcp(self, enable): + """ + Enable/Disable DHCP client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_base = r'/var/lib/dhcp/dhclient' + config_file = f'{config_base}_{ifname}.conf' + options_file = f'{config_base}_{ifname}.options' + pid_file = f'{config_base}_{ifname}.pid' + lease_file = f'{config_base}_{ifname}.leases' + + if enable and 'disable' not in self._config: + if vyos_dict_search('dhcp_options.host_name', self._config) == None: + # read configured system hostname. + # maybe change to vyos hostd client ??? + hostname = 'vyos' + with open('/etc/hostname', 'r') as f: + hostname = f.read().rstrip('\n') + tmp = {'dhcp_options' : { 'host_name' : hostname}} + self._config = dict_merge(tmp, self._config) + + render(options_file, 'dhcp-client/daemon-options.tmpl', + self._config, trim_blocks=True) + render(config_file, 'dhcp-client/ipv4.tmpl', + self._config, trim_blocks=True) + + # 'up' check is mandatory b/c even if the interface is A/D, as soon as + # the DHCP client is started the interface will be placed in u/u state. + # This is not what we intended to do when disabling an interface. + return self._cmd(f'systemctl restart dhclient@{ifname}.service') + else: + self._cmd(f'systemctl stop dhclient@{ifname}.service') + + # cleanup old config files + for file in [config_file, options_file, pid_file, lease_file]: + if os.path.isfile(file): + os.remove(file) + + + def set_dhcpv6(self, enable): + """ + Enable/Disable DHCPv6 client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + + if enable and 'disable' not in self._config: + render(config_file, 'dhcp-client/ipv6.tmpl', + self._config, trim_blocks=True) + + # We must ignore any return codes. This is required to enable DHCPv6-PD + # for interfaces which are yet not up and running. + return self._popen(f'systemctl restart dhcp6c@{ifname}.service') + else: + self._popen(f'systemctl stop dhcp6c@{ifname}.service') + + if os.path.isfile(config_file): + os.remove(config_file) + + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface. """ + # Cache the configuration - it will be reused inside e.g. DHCP handler + # XXX: maybe pass the option via __init__ in the future and rename this + # method to apply()? + self._config = config + # Update interface description - self.set_alias(config.get('description', None)) + self.set_alias(config.get('description', '')) + + # Ignore link state changes + value = '2' if 'disable_link_detect' in config else '1' + self.set_link_detect(value) # Configure assigned interface IP addresses. No longer # configured addresses will be removed first new_addr = config.get('address', []) - # XXX workaround for T2636, convert IP address string to a list - # with one element - if isinstance(new_addr, str): - new_addr = [new_addr] + # always ensure DHCP client is stopped (when not configured explicitly) + if 'dhcp' not in new_addr: + self.del_addr('dhcp') + + # always ensure DHCPv6 client is stopped (when not configured as client + # for IPv6 address or prefix delegation + dhcpv6pd = vyos_dict_search('dhcpv6_options.pd', config) + if 'dhcpv6' not in new_addr or dhcpv6pd == None: + self.del_addr('dhcpv6') # determine IP addresses which are assigned to the interface and build a # list of addresses which are no longer in the dict so they can be removed @@ -786,13 +917,144 @@ class Interface(Control): for addr in new_addr: self.add_addr(addr) + # start DHCPv6 client when only PD was configured + if dhcpv6pd != None: + self.set_dhcpv6(True) + # There are some items in the configuration which can only be applied # if this instance is not bound to a bridge. This should be checked # by the caller but better save then sorry! - if not config.get('is_bridge_member', False): - # Bind interface instance into VRF + if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): + # Bind interface to given VRF or unbind it if vrf node is not set. + # unbinding will call 'ip link set dev eth0 nomaster' which will + # also drop the interface out of a bridge or bond - thus this is + # checked before self.set_vrf(config.get('vrf', '')) - # Interface administrative state - state = 'down' if 'disable' in config.keys() else 'up' - self.set_admin_state(state) + # Configure ARP cache timeout in milliseconds - has default value + tmp = vyos_dict_search('ip.arp_cache_timeout', config) + value = tmp if (tmp != None) else '30' + self.set_arp_cache_tmo(value) + + # Configure ARP filter configuration + tmp = vyos_dict_search('ip.disable_arp_filter', config) + value = '0' if (tmp != None) else '1' + self.set_arp_filter(value) + + # Configure ARP accept + tmp = vyos_dict_search('ip.enable_arp_accept', config) + value = '1' if (tmp != None) else '0' + self.set_arp_accept(value) + + # Configure ARP announce + tmp = vyos_dict_search('ip.enable_arp_announce', config) + value = '1' if (tmp != None) else '0' + self.set_arp_announce(value) + + # Configure ARP ignore + tmp = vyos_dict_search('ip.enable_arp_ignore', config) + value = '1' if (tmp != None) else '0' + self.set_arp_ignore(value) + + # Enable proxy-arp on this interface + tmp = vyos_dict_search('ip.enable_proxy_arp', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp(value) + + # Enable private VLAN proxy ARP on this interface + tmp = vyos_dict_search('ip.proxy_arp_pvlan', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp_pvlan(value) + + # IPv6 forwarding + tmp = vyos_dict_search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + self.set_ipv6_forwarding(value) + + # IPv6 router advertisements + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '2' if (tmp != None) else '1' + if 'dhcpv6' in new_addr: + value = '2' + self.set_ipv6_accept_ra(value) + + # IPv6 address autoconfiguration + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '1' if (tmp != None) else '0' + self.set_ipv6_autoconf(value) + + # IPv6 Duplicate Address Detection (DAD) tries + tmp = vyos_dict_search('ipv6.dup_addr_detect_transmits', config) + value = tmp if (tmp != None) else '1' + self.set_ipv6_dad_messages(value) + + # MTU - Maximum Transfer Unit + if 'mtu' in config: + self.set_mtu(config.get('mtu')) + + # Delete old IPv6 EUI64 addresses before changing MAC + tmp = vyos_dict_search('ipv6.address.eui64_old', config) + if tmp: + for addr in tmp: + self.del_ipv6_eui64_address(addr) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed. Skip if bond member. + if 'is_bond_member' not in config: + mac = config.get('hw_id') + if 'mac' in config: + mac = config.get('mac') + if mac: + self.set_mac(mac) + + # Manage IPv6 link-local addresses + tmp = vyos_dict_search('ipv6.address.no_default_link_local', config) + # we must check explicitly for None type as if the key is set we will + # get an empty dict (<class 'dict'>) + if tmp is not None: + self.del_ipv6_eui64_address('fe80::/64') + else: + self.add_ipv6_eui64_address('fe80::/64') + + # Add IPv6 EUI-based addresses + tmp = vyos_dict_search('ipv6.address.eui64', config) + if tmp: + for addr in tmp: + self.add_ipv6_eui64_address(addr) + + # re-add ourselves to any bridge we might have fallen out of + if 'is_bridge_member' in config: + bridge = config.get('is_bridge_member') + self.add_to_bridge(bridge) + + # remove no longer required 802.1ad (Q-in-Q VLANs) + for vif_s_id in config.get('vif_s_remove', {}): + self.del_vlan(vif_s_id) + + # create/update 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] + for vif_s_id, vif_s in config.get('vif_s', {}).items(): + tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) + s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) + vif_s['ifname'] = f'{ifname}.{vif_s_id}' + s_vlan.update(vif_s) + + # remove no longer required client VLAN (vif-c) + for vif_c_id in vif_s.get('vif_c_remove', {}): + s_vlan.del_vlan(vif_c_id) + + # create/update client VLAN (vif-c) interface + for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): + c_vlan = s_vlan.add_vlan(vif_c_id) + vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' + c_vlan.update(vif_c) + + # remove no longer required 802.1q VLAN interfaces + for vif_id in config.get('vif_remove', {}): + self.del_vlan(vif_id) + + # create/update 802.1q VLAN interfaces + for vif_id, vif in config.get('vif', {}).items(): + vlan = self.add_vlan(vif_id) + vif['ifname'] = f'{ifname}.{vif_id}' + vlan.update(vif) diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index 7ebd13b54..c70e1773f 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -64,16 +64,21 @@ class LoopbackIf(Interface): on any interface. """ addr = config.get('address', []) - # XXX workaround for T2636, convert IP address string to a list - # with one element - if isinstance(addr, str): - addr = [addr] - # We must ensure that the loopback addresses are never deleted from the system addr += self._persistent_addresses # Update IP address entry in our dictionary config.update({'address' : addr}) - # now call the regular function from within our base class + # call base class super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index ea8c9807e..6f570d162 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -71,3 +71,22 @@ class MACsecIf(Interface): 'source_interface': '', } return config + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index b5481f4a7..b068ce873 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -68,3 +68,22 @@ class MACVLANIf(Interface): >> dict = MACVLANIf().get_config() """ return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index 5e6387881..01a7cc7ab 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -96,7 +96,7 @@ class VRRP(object): pid = util.read_file(cls.location['pid']) os.kill(int(pid), cls._signal[what]) - # shoud look for file size change ? + # should look for file size change? sleep(0.2) return util.read_file(fname) except FileNotFoundError: @@ -126,8 +126,8 @@ class VRRP(object): return disabled @classmethod - def format (cls, data): - headers = ["Name", "Interface", "VRID", "State", "Last Transition"] + def format(cls, data): + headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"] groups = [] data = json.loads(data) @@ -138,11 +138,12 @@ class VRRP(object): intf = data['ifp_ifname'] vrid = data['vrid'] state = cls.decode_state(data["state"]) + priority = data['effective_priority'] since = int(time() - float(data['last_transition'])) last = util.seconds_to_human(since) - groups.append([name, intf, vrid, state, last]) + groups.append([name, intf, vrid, state, priority, last]) # add to the active list disabled instances groups.extend(cls.disabled()) diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 973b4ef05..18a500336 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -47,8 +47,8 @@ class VXLANIf(Interface): 'port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port 'remote': '', - 'src_address': '', - 'src_interface': '', + 'source_address': '', + 'source_interface': '', 'vni': 0 } definition = { @@ -60,29 +60,29 @@ class VXLANIf(Interface): } } options = Interface.options + \ - ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] + ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address'] mapping = { 'ifname': 'add', 'vni': 'id', 'port': 'dstport', - 'src_address': 'local', - 'src_interface': 'dev', + 'source_address': 'local', + 'source_interface': 'dev', } def _create(self): cmdline = ['ifname', 'type', 'vni', 'port'] - if self.config['src_address']: - cmdline.append('src_address') + if self.config['source_address']: + cmdline.append('source_address') if self.config['remote']: cmdline.append('remote') - if self.config['group'] or self.config['src_interface']: - if self.config['group'] and self.config['src_interface']: + if self.config['group'] or self.config['source_interface']: + if self.config['group'] and self.config['source_interface']: cmdline.append('group') - cmdline.append('src_interface') + cmdline.append('source_interface') else: ifname = self.config['ifname'] raise ConfigError( @@ -109,3 +109,22 @@ class VXLANIf(Interface): >> dict = VXLANIf().get_config() """ return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 62ca57ca2..fad4ef282 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -24,7 +24,7 @@ from hurry.filesize import alternative from vyos.config import Config from vyos.ifconfig import Interface from vyos.ifconfig import Operational - +from vyos.validate import is_ipv6 class WireGuardOperational(Operational): def _dump(self): @@ -169,65 +169,79 @@ class WireGuardIf(Interface): ['port', 'private_key', 'pubkey', 'psk', 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] - """ - Wireguard interface class, contains a comnfig dictionary since - wireguard VPN is being comnfigured via the wg command rather than - writing the config into a file. Otherwise if a pre-shared key is used - (symetric enryption key), it would we exposed within multiple files. - Currently it's only within the config.boot if the config was saved. - - Example: - >>> from vyos.ifconfig import WireGuardIf as wg_if - >>> wg_intfc = wg_if("wg01") - >>> print (wg_intfc.wg_config) - {'private_key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, - 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - >>> wg_intfc.wg_config['keepalive'] = 100 - >>> print (wg_intfc.wg_config) - {'private_key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, - 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - """ - - def update(self): - if not self.config['private_key']: - raise ValueError("private key required") - else: - # fmask permission check? - pass - - cmd = 'wg set {ifname}'.format(**self.config) - cmd += ' listen-port {port}'.format(**self.config) - cmd += ' fwmark "{fwmark}" '.format(**self.config) - cmd += ' private-key {private_key}'.format(**self.config) - cmd += ' peer {pubkey}'.format(**self.config) - cmd += ' persistent-keepalive {keepalive}'.format(**self.config) - # allowed-ips must be properly quoted else the interface can't be properly - # created as the wg utility will tread multiple IP addresses as command - # parameters - cmd += ' allowed-ips "{}"'.format(','.join(self.config['allowed-ips'])) - - if self.config['endpoint']: - cmd += ' endpoint "{endpoint}"'.format(**self.config) - - psk_file = '' - if self.config['psk']: - psk_file = '/tmp/{ifname}.psk'.format(**self.config) - with open(psk_file, 'w') as f: - f.write(self.config['psk']) + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # remove no longer associated peers first + if 'peer_remove' in config: + for tmp in config['peer_remove']: + peer = config['peer_remove'][tmp] + peer['ifname'] = config['ifname'] + + cmd = 'wg set {ifname} peer {pubkey} remove' + self._cmd(cmd.format(**peer)) + + # Wireguard base command is identical for every peer + base_cmd = 'wg set {ifname} private-key {private_key}' + if 'port' in config: + base_cmd += ' listen-port {port}' + if 'fwmark' in config: + base_cmd += ' fwmark {fwmark}' + + base_cmd = base_cmd.format(**config) + + for tmp in config['peer']: + peer = config['peer'][tmp] + + # start of with a fresh 'wg' command + cmd = base_cmd + ' peer {pubkey}' + + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + no_psk_file = '/dev/null' + psk_file = no_psk_file + if 'preshared_key' in peer: + psk_file = '/tmp/tmp.wireguard.psk' + with open(psk_file, 'w') as f: + f.write(peer['preshared_key']) cmd += f' preshared-key {psk_file}' - self._cmd(cmd) - - # PSK key file is not required to be stored persistently as its backed by CLI - if os.path.exists(psk_file): - os.remove(psk_file) - - def remove_peer(self, peerkey): - """ - Remove a peer of an interface, peers are identified by their public key. - Giving it a readable name is a vyos feature, to remove a peer the pubkey - and the interface is needed, to remove the entry. - """ - cmd = "wg set {0} peer {1} remove".format( - self.config['ifname'], str(peerkey)) - return self._cmd(cmd) + # Persistent keepalive is optional + if 'persistent_keepalive'in peer: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer['allowed_ips'], str): + peer['allowed_ips'] = [peer['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer['allowed_ips']) + + # Endpoint configuration is optional + if {'address', 'port'} <= set(peer): + if is_ipv6(config['address']): + cmd += ' endpoint [{address}]:{port}' + else: + cmd += ' endpoint {address}:{port}' + + self._cmd(cmd.format(**peer)) + + # PSK key file is not required to be stored persistently as its backed by CLI + if psk_file != no_psk_file and os.path.exists(psk_file): + os.remove(psk_file) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 3122ac0a3..a50346ffa 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -71,6 +71,25 @@ class WiFiIf(Interface): return config + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + @Interface.register class WiFiModemIf(WiFiIf): diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py deleted file mode 100644 index 442cb0db8..000000000 --- a/python/vyos/ifconfig_vlan.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -from netifaces import interfaces -from vyos import ConfigError - -def apply_all_vlans(intf, intfconfig): - """ - Function applies all VLANs to the passed interface. - - intf: object of Interface class - intfconfig: dict with interface configuration - """ - # remove no longer required service VLAN interfaces (vif-s) - for vif_s in intfconfig['vif_s_remove']: - intf.del_vlan(vif_s) - - # create service VLAN interfaces (vif-s) - for vif_s_id, vif_s in intfconfig['vif_s'].items(): - s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype']) - apply_vlan_config(s_vlan, vif_s) - - # remove no longer required client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c_remove']: - s_vlan.del_vlan(vif_c) - - # create client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c_id, vif_c in vif_s['vif_c'].items(): - c_vlan = s_vlan.add_vlan(vif_c_id) - apply_vlan_config(c_vlan, vif_c) - - # remove no longer required VLAN interfaces (vif) - for vif in intfconfig['vif_remove']: - intf.del_vlan(vif) - - # create VLAN interfaces (vif) - for vif_id, vif in intfconfig['vif'].items(): - # QoS priority mapping can only be set during interface creation - # so we delete the interface first if required. - if vif['egress_qos_changed'] or vif['ingress_qos_changed']: - try: - # on system bootup the above condition is true but the interface - # does not exists, which throws an exception, but that's legal - intf.del_vlan(vif_id) - except: - pass - - vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) - apply_vlan_config(vlan, vif) - - -def apply_vlan_config(vlan, config): - """ - Generic function to apply a VLAN configuration from a dictionary - to a VLAN interface - """ - - if not vlan.definition['vlan']: - raise TypeError() - - if config['dhcp_client_id']: - vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id'] - - if config['dhcp_hostname']: - vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname'] - - if config['dhcp_vendor_class_id']: - vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id'] - - if config['dhcpv6_prm_only']: - vlan.dhcp.v6.options['dhcpv6_prm_only'] = True - - if config['dhcpv6_temporary']: - vlan.dhcp.v6.options['dhcpv6_temporary'] = True - - if config['dhcpv6_pd_length']: - vlan.dhcp.v6.options['dhcpv6_pd_length'] = config['dhcpv6_pd_length'] - - if config['dhcpv6_pd_interfaces']: - vlan.dhcp.v6.options['dhcpv6_pd_interfaces'] = config['dhcpv6_pd_interfaces'] - - # update interface description used e.g. within SNMP - vlan.set_alias(config['description']) - # ignore link state changes - vlan.set_link_detect(config['disable_link_detect']) - # configure ARP filter configuration - vlan.set_arp_filter(config['ip_disable_arp_filter']) - # configure ARP accept - vlan.set_arp_accept(config['ip_enable_arp_accept']) - # configure ARP announce - vlan.set_arp_announce(config['ip_enable_arp_announce']) - # configure ARP ignore - vlan.set_arp_ignore(config['ip_enable_arp_ignore']) - # configure Proxy ARP - vlan.set_proxy_arp(config['ip_proxy_arp']) - # IPv6 accept RA - vlan.set_ipv6_accept_ra(config['ipv6_accept_ra']) - # IPv6 address autoconfiguration - vlan.set_ipv6_autoconf(config['ipv6_autoconf']) - # IPv6 forwarding - vlan.set_ipv6_forwarding(config['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect']) - # Maximum Transmission Unit (MTU) - vlan.set_mtu(config['mtu']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not config['is_bridge_member']: - vlan.set_vrf(config['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in config['ipv6_eui64_prefix_remove']: - vlan.del_ipv6_eui64_address(addr) - - # Change VLAN interface MAC address - if config['mac']: - vlan.set_mac(config['mac']) - - # Add IPv6 EUI-based addresses - for addr in config['ipv6_eui64_prefix']: - vlan.add_ipv6_eui64_address(addr) - - # enable/disable VLAN interface - if config['disable']: - vlan.set_admin_state('down') - else: - vlan.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - - # re-add ourselves to any bridge we might have fallen out of - if config['is_bridge_member']: - vlan.add_to_bridge(config['is_bridge_member']) - -def verify_vlan_config(config): - """ - Generic function to verify VLAN config consistency. Instead of re- - implementing this function in multiple places use single source \o/ - """ - - # config['vif'] is a dict with ids as keys and config dicts as values - for vif in config['vif'].values(): - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - if ( vif['is_bridge_member'] - and ( vif['address'] - or vif['ipv6_eui64_prefix'] - or vif['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif interface {vif["intf"]} ' - f'which is a member of bridge {vif["is_bridge_member"]}')) - - if vif['vrf']: - if vif['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif["vrf"]}" does not exist') - - if vif['is_bridge_member']: - raise ConfigError(( - f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} ' - f'and bridge {vif["is_bridge_member"]} at the same time!')) - - # e.g. wireless interface has no vif_s support - # thus we bail out eraly. - if 'vif_s' not in config.keys(): - return - - # config['vif_s'] is a dict with ids as keys and config dicts as values - for vif_s_id, vif_s in config['vif_s'].items(): - for vif_id, vif in config['vif'].items(): - if vif_id == vif_s_id: - raise ConfigError(( - f'Cannot use identical ID on vif "{vif["intf"]}" ' - f'and vif-s "{vif_s["intf"]}"')) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError(( - 'DHCPv6 temporary and parameters-only options are mutually ' - 'exclusive!')) - - if ( vif_s['is_bridge_member'] - and ( vif_s['address'] - or vif_s['ipv6_eui64_prefix'] - or vif_s['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif-s interface {vif_s["intf"]} ' - f'which is a member of bridge {vif_s["is_bridge_member"]}')) - - if vif_s['vrf']: - if vif_s['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist') - - if vif_s['is_bridge_member']: - raise ConfigError(( - f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} ' - f'and bridge {vif_s["is_bridge_member"]} at the same time!')) - - # vif_c is a dict with ids as keys and config dicts as values - for vif_c in vif_s['vif_c'].values(): - # DHCPv6 parameters-only and temporary address are mutually exclusive - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError(( - 'DHCPv6 temporary and parameters-only options are ' - 'mutually exclusive!')) - - if ( vif_c['is_bridge_member'] - and ( vif_c['address'] - or vif_c['ipv6_eui64_prefix'] - or vif_c['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to vif-c interface {vif_c["intf"]} ' - f'which is a member of bridge {vif_c["is_bridge_member"]}')) - - if vif_c['vrf']: - if vif_c['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist') - - if vif_c['is_bridge_member']: - raise ConfigError(( - f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} ' - f'and bridge {vif_c["is_bridge_member"]} at the same time!')) - diff --git a/python/vyos/template.py b/python/vyos/template.py index d9b0c749d..c88ab04a0 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,78 +13,131 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +import functools import os +from ipaddress import ip_network from jinja2 import Environment from jinja2 import FileSystemLoader from vyos.defaults import directories from vyos.util import chmod, chown, makedir -# reuse the same Environment to improve performance -_templates_env = { - False: Environment(loader=FileSystemLoader(directories['templates'])), - True: Environment(loader=FileSystemLoader(directories['templates']), trim_blocks=True), -} -_templates_mem = { - False: {}, - True: {}, -} -def vyos_address_from_cidr(text): - """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". - Example: - 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: - """ - from ipaddress import ip_network - return ip_network(text).network_address +# Holds template filters registered via register_filter() +_FILTERS = {} -def vyos_netmask_from_cidr(text): - """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask". - Example: - 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff:: - """ - from ipaddress import ip_network - return ip_network(text).netmask -def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): +# reuse Environments with identical trim_blocks setting to improve performance +@functools.lru_cache(maxsize=2) +def _get_environment(trim_blocks): + env = Environment( + # Don't check if template files were modified upon re-rendering + auto_reload=False, + # Cache up to this number of templates for quick re-rendering + cache_size=100, + loader=FileSystemLoader(directories["templates"]), + trim_blocks=trim_blocks, + ) + env.filters.update(_FILTERS) + return env + + +def register_filter(name, func=None): + """Register a function to be available as filter in templates under given name. + + It can also be used as a decorator, see below in this module for examples. + + :raise RuntimeError: + when trying to register a filter after a template has been rendered already + :raise ValueError: when trying to register a name which was taken already """ - render a template from the template directory, it will raise on any errors - destination: the file where the rendered template must be saved - template: the path to the template relative to the template folder - content: the dictionary to use to render the template - - This classes cache the renderer, so rendering the same file multiple time - does not cause as too much overhead. If use everywhere, it could be changed - and load the template from python environement variables from an import - python module generated when the debian package is build - (recovering the load time and overhead caused by having the file out of the code) + if func is None: + return functools.partial(register_filter, name) + if _get_environment.cache_info().currsize: + raise RuntimeError( + "Filters can only be registered before rendering the first template" + ) + if name in _FILTERS: + raise ValueError(f"A filter with name {name!r} was registered already") + _FILTERS[name] = func + return func + + +def render_to_string(template, content, trim_blocks=False, formater=None): + """Render a template from the template directory, raise on any errors. + + :param template: the path to the template relative to the template folder + :param content: the dictionary of variables to put into rendering context + :param trim_blocks: controls the trim_blocks jinja2 feature + :param formater: + if given, it has to be a callable the rendered string is passed through + + The parsed template files are cached, so rendering the same file multiple times + does not cause as too much overhead. + If used everywhere, it could be changed to load the template from Python + environment variables from an importable Python module generated when the Debian + package is build (recovering the load time and overhead caused by having the + file out of the code). """ + template = _get_environment(bool(trim_blocks)).get_template(template) + rendered = template.render(content) + if formater is not None: + rendered = formater(rendered) + return rendered + + +def render( + destination, + template, + content, + trim_blocks=False, + formater=None, + permission=None, + user=None, + group=None, +): + """Render a template from the template directory to a file, raise on any errors. - # Create the directory if it does not exists + :param destination: path to the file to save the rendered template in + :param permission: permission bitmask to set for the output file + :param user: user to own the output file + :param group: group to own the output file + + All other parameters are as for :func:`render_to_string`. + """ + # Create the directory if it does not exist folder = os.path.dirname(destination) makedir(folder, user, group) - # Setup a renderer for the given template - # This is cached and re-used for performance - if template not in _templates_mem[trim_blocks]: - _env = _templates_env[trim_blocks] - _env.filters['address_from_cidr'] = vyos_address_from_cidr - _env.filters['netmask_from_cidr'] = vyos_netmask_from_cidr - _templates_mem[trim_blocks][template] = _env.get_template(template) + # As we are opening the file with 'w', we are performing the rendering before + # calling open() to not accidentally erase the file if rendering fails + rendered = render_to_string(template, content, trim_blocks, formater) - template = _templates_mem[trim_blocks][template] + # Write to file + with open(destination, "w") as file: + chmod(file.fileno(), permission) + chown(file.fileno(), user, group) + file.write(rendered) - # As we are opening the file with 'w', we are performing the rendering - # before calling open() to not accidentally erase the file if the - # templating fails - content = template.render(content) - if formater: - content = formater(content) +################################## +# Custom template filters follow # +################################## - # Write client config file - with open(destination, 'w') as f: - f.write(content) - chmod(destination, permission) - chown(destination, user, group) +@register_filter("address_from_cidr") +def vyos_address_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". + Example: + 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: + """ + return ip_network(text).network_address + + +@register_filter("netmask_from_cidr") +def vyos_netmask_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask". + Example: + 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff:: + """ + return ip_network(text).netmask diff --git a/python/vyos/util.py b/python/vyos/util.py index 924df6b3a..84aa16791 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -240,9 +240,10 @@ def chown(path, user, group): if user is None or group is None: return False - if not os.path.exists(path): + # path may also be an open file descriptor + if not isinstance(path, int) and not os.path.exists(path): return False - + uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid os.chown(path, uid, gid) @@ -250,7 +251,8 @@ def chown(path, user, group): def chmod(path, bitmask): - if not os.path.exists(path): + # path may also be an open file descriptor + if not isinstance(path, int) and not os.path.exists(path): return if bitmask is None: return @@ -261,28 +263,25 @@ def chmod_600(path): """ make file only read/writable by owner """ from stat import S_IRUSR, S_IWUSR - if os.path.exists(path): - bitmask = S_IRUSR | S_IWUSR - os.chmod(path, bitmask) + bitmask = S_IRUSR | S_IWUSR + chmod(path, bitmask) def chmod_750(path): """ make file/directory only executable to user and group """ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP - if os.path.exists(path): - bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP - os.chmod(path, bitmask) + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP + chmod(path, bitmask) def chmod_755(path): """ make file executable by all """ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH - if os.path.exists(path): - bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ - S_IROTH | S_IXOTH - os.chmod(path, bitmask) + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + chmod(path, bitmask) def makedir(path, user=None, group=None): @@ -652,3 +651,38 @@ def get_bridge_member_config(conf, br, intf): conf.set_level(old_level) return memberconf + +def check_kmod(k_mod): + """ Common utility function to load required kernel modules on demand """ + if isinstance(k_mod, str): + k_mod = k_mod.split() + for module in k_mod: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') + +def find_device_file(device): + """ Recurively search /dev for the given device file and return its full path. + If no device file was found 'None' is returned """ + from fnmatch import fnmatch + + for root, dirs, files in os.walk('/dev'): + for basename in files: + if fnmatch(basename, device): + return os.path.join(root, basename) + + return None + +def vyos_dict_search(path, dict): + """ Traverse Python dictionary (dict) delimited by dot (.). + Return value of key if found, None otherwise. + + This is faster implementation then jmespath.search('foo.bar', dict)""" + parts = path.split('.') + inside = parts[:-1] + if not inside: + return dict[path] + c = dict + for p in parts[:-1]: + c = c.get(p, {}) + return c.get(parts[-1], None) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 9072c5817..ceeb6888a 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -19,6 +19,7 @@ import netifaces import ipaddress from vyos.util import cmd +from vyos import xml # Important note when you are adding new validation functions: # @@ -278,7 +279,6 @@ def is_member(conf, interface, intftype=None): False -> interface type cannot have members """ ret_val = None - if intftype not in ['bonding', 'bridge', None]: raise ValueError(( f'unknown interface type "{intftype}" or it cannot ' @@ -291,14 +291,14 @@ def is_member(conf, interface, intftype=None): conf.set_level([]) for it in intftype: - base = 'interfaces ' + it + base = ['interfaces', it] for intf in conf.list_nodes(base): - memberintf = f'{base} {intf} member interface' - if conf.is_tag(memberintf): + memberintf = base + [intf, 'member', 'interface'] + if xml.is_tag(memberintf): if interface in conf.list_nodes(memberintf): ret_val = intf break - elif conf.is_leaf(memberintf): + elif xml.is_leaf(memberintf): if ( conf.exists(memberintf) and interface in conf.return_values(memberintf) ): ret_val = intf diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py index 6e0e73b1b..0ef0c85ce 100644 --- a/python/vyos/xml/__init__.py +++ b/python/vyos/xml/__init__.py @@ -35,10 +35,29 @@ def load_configuration(cache=[]): return xml +# def is_multi(lpath): +# return load_configuration().is_multi(lpath) + + +def is_tag(lpath): + return load_configuration().is_tag(lpath) + + +def is_leaf(lpath, flat=True): + return load_configuration().is_leaf(lpath, flat) + + def defaults(lpath, flat=False): return load_configuration().defaults(lpath, flat) +def multi_to_list(lpath, conf): + return load_configuration().multi_to_list(lpath, conf) + + if __name__ == '__main__': print(defaults(['service'], flat=True)) print(defaults(['service'], flat=False)) + + print(is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + print(is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py index 5421007e0..a25fc50c5 100644 --- a/python/vyos/xml/definition.py +++ b/python/vyos/xml/definition.py @@ -126,10 +126,12 @@ class XML(dict): elif word: if data_node != kw.plainNode or len(passed) == 1: self.options = [_ for _ in self.tree if _.startswith(word)] + self.options.sort() else: self.options = [] else: self.options = named_options + self.options.sort() self.plain = not is_dataNode @@ -143,6 +145,7 @@ class XML(dict): self.word = '' if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): self.options = [_ for _ in self.tree if not kw.found(_)] + self.options.sort() def checks(self, cmd): # as we move thought the named node twice @@ -248,9 +251,12 @@ class XML(dict): def defaults(self, lpath, flat): d = self[kw.default] for k in lpath: - d = d[k] + d = d.get(k, {}) if not flat: + # _flatten will make this conversion + d = self.multi_to_list(lpath, d) + r = {} for k in d: under = k.replace('-','_') @@ -278,6 +284,23 @@ class XML(dict): return _flatten(lpath, len(lpath), d) + def multi_to_list(self, lpath, conf): + r = {} + for k in conf: + # key mangling could also be done here + # it would prevent two parsing of the config tree + # under = k.replace('-','_') + under = k + fpath = lpath + [k] + if isinstance(conf[k],dict): + r[under] = self.multi_to_list(fpath, conf[k]) + continue + value = conf[k] + if self.is_multi(fpath) and not isinstance(value, list): + value = [value] + r[under] = value + return r + # from functools import lru_cache # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later @@ -300,16 +323,28 @@ class XML(dict): return tree def _get(self, lpath, tag, with_tag=True): - return self._tree(lpath + [tag], with_tag) + tree = self._tree(lpath, with_tag) + if tree is None: + return None + return tree.get(tag, None) def is_multi(self, lpath, with_tag=True): - return self._get(lpath, kw.multi, with_tag) is True + tree = self._get(lpath, kw.multi, with_tag) + if tree is None: + return None + return tree is True def is_tag(self, lpath, with_tag=True): - return self._get(lpath, kw.node, with_tag) == kw.tagNode + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.tagNode def is_leaf(self, lpath, with_tag=True): - return self._get(lpath, kw.node, with_tag) == kw.leafNode + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.leafNode def exists(self, lpath, with_tag=True): return self._get(lpath, kw.node, with_tag) is not None diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py index ac0620d99..ff55151d2 100644 --- a/python/vyos/xml/test_xml.py +++ b/python/vyos/xml/test_xml.py @@ -33,7 +33,7 @@ class TestSearch(TestCase): last = self.xml.traverse("") self.assertEqual(last, '') self.assertEqual(self.xml.inside, []) - self.assertEqual(self.xml.options, ['protocols', 'service', 'system', 'firewall', 'interfaces', 'vpn', 'nat', 'vrf', 'high-availability']) + self.assertEqual(self.xml.options, ['firewall', 'high-availability', 'interfaces', 'nat', 'protocols', 'service', 'system', 'vpn', 'vrf']) self.assertEqual(self.xml.filling, False) self.assertEqual(self.xml.word, last) self.assertEqual(self.xml.check, False) diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 689d19ece..c60b32a1e 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -111,7 +111,7 @@ def get_properties(p): for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: - comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text)) + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) diff --git a/scripts/build-command-templates b/scripts/build-command-templates index 457adbec2..d6585b0cc 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -225,11 +225,13 @@ def make_node_def(props): if "constraint" in props: node_def += "syntax:expression: {0}\n".format(props["constraint"]) + shim = '${vyshim}' + if "owner" in props: if "tag" in props: - node_def += "end: sudo sh -c \"VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"]) + node_def += "end: sudo sh -c \"{1} VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"], shim) else: - node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"]) + node_def += "end: sudo sh -c \"{1} {0}\"\n".format(props["owner"], shim) if debug: print("The contents of the node.def file:\n", node_def) diff --git a/scripts/update-configd-include-file b/scripts/update-configd-include-file new file mode 100755 index 000000000..6615e21ff --- /dev/null +++ b/scripts/update-configd-include-file @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +### +# A simple script for safely editing configd-include.json, the list of +# scripts which are off-loaded to be run by the daemon. +# Usage: +# update-configd-include-file --add script1.py script2.py ... +# --remove scriptA.py scriptB.py ... +# +# Additionally, it offers optional sanity checks by examining the signatures +# of functions and placement of Config instance for consistency with configd +# requirements. +# Usage: +# update-configd-include-file --check-current +# to check the current include list +# update-configd-include-file --check-file +# to check arbitrary conf_mode scripts +# +# Note that this feature is the basis for the configd smoketest, but it is of +# limited use in this script, as it requires an environment that has all script +# (python) dependencies installed (e.g. installed image) so that the script may +# be imported for introspection. Nonetheless, for testing and development, it has +# its uses. + +import os +import sys +import json +import argparse +import datetime +import importlib.util +from inspect import signature, getsource + +from vyos.defaults import directories +from vyos.version import get_version +from vyos.util import cmd + +# Defaults + +installed_image = False + +include_file = 'configd-include.json' +build_relative_include_file = '../data/configd-include.json' +dirname = os.path.dirname(__file__) + +build_location_include_file = os.path.join(dirname, build_relative_include_file) +image_location_include_file = os.path.join(directories['data'], include_file) + +build_relative_conf_dir = '../src/conf_mode' + +build_location_conf_dir = os.path.join(dirname, build_relative_conf_dir) +image_location_conf_dir = directories['conf_mode'] + +# Get arguments + +parser = argparse.ArgumentParser(description='Add or remove scripts from the list of scripts to be run be daemon') +parser.add_argument('--add', nargs='*', default=[], + help='scripts to add to configd include list') +parser.add_argument('--remove', nargs='*', default=[], + help='scripts to remove from configd include list') +parser.add_argument('--show-diff', action='store_true', + help='show list of conf_mode scripts not in include list') +parser.add_argument('--check-file', nargs='*', default=[], + help='check files for suitability to run under daemon') +parser.add_argument('--check-current', action="store_true", + help='check current include list for suitability to run under daemon') + +args = vars(parser.parse_args()) + +# Check if we are running within installed image; since this script is not +# part of the distribution, there is no need to check if live cd +if get_version(): + installed_image = True + +if installed_image: + include_file = image_location_include_file + conf_dir = image_location_conf_dir +else: + include_file = build_location_include_file + conf_dir = build_location_conf_dir + +# Utilities for checking function signature and body +def import_script(s: str): + """ + A compact form of the import code in vyos-configd + """ + path = os.path.join(conf_dir, s) + if not os.path.exists(path): + print(f"script {s} is not in conf_mode directory") + return None + + name = os.path.splitext(s)[0].replace('-', '_') + + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + return module + +funcs = { 'get_config': False, + 'verify': False, + 'generate': False, + 'apply': False + } + +def check_signatures(s: str) -> bool: + """ + Basic sanity check: script standard functions should all take one + argument, including get_config(config=None). + """ + funcd = dict(funcs) + for i in list(funcd): + m = import_script(s) + f = getattr(m, i, None) + if not f: + funcd[i] = True + continue + sig = signature(f) + params = sig.parameters + if len(params) != 1: + continue + if i == 'get_config': + for p in params.values(): + funcd[i] = True if (p.default is None) else False + else: + funcd[i] = True + + res = True + + for k, v in funcd.items(): + if v is False: + if k == 'get_config': + print(f"function '{k}' will need the standard modification") + else: + print(f"function '{k}' in script '{s}' has wrong signature") + res = False + + return res + +def check_instance_per_function(s: str) -> bool: + """ + The standard function 'get_config' should have one instantiation of Config; + all other standard functions, zero. + """ + funcd = dict(funcs) + for i in list(funcd): + m = import_script(s) + f = getattr(m, i, None) + if not f: + funcd[i] = True + continue + str_f = getsource(f) + n = str_f.count('Config()') + if n == 1 and i == 'get_config': + funcd[i] = True + if n == 0 and i != 'get_config': + funcd[i] = True + + res = True + + for k, v in funcd.items(): + if v is False: + fi = 'zero' if k == 'get_config' else 'non-zero' + print(f"function '{k}' in script '{s}' has {fi} instances of Config") + res = False + + return res + +def check_instance_total(s: str) -> bool: + """ + A script should have at most one instantiation of Config. + """ + m = import_script(s) + str_m = getsource(m) + n = str_m.count('Config()') + if n != 1: + print(f"instance of Config outside of 'get_config' in script '{s}'") + return False + + return True + +def check_config_modification(s: str) -> bool: + """ + Modification to the session config from within a script is necessary in + certain cases, but the script should then run as stand-alone. + """ + m = import_script(s) + str_m = getsource(m) + n = str_m.count('my_set') + if n != 0: + print(f"modification of config within script") + return False + + return True + +def check_viability(s: str) -> bool: + """ + Check existence, and if on installed image, signatures, instances of + Config, and modification of session config + """ + path = os.path.join(conf_dir, s) + if not os.path.exists(path): + print(f"script {s} is not in conf_mode directory") + return False + + if not installed_image: + if args['check_file'] or args['check_current']: + print(f"In order to check script viability for offload, run this script on installed image") + return True + + r1 = check_signatures(s) + r2 = check_instance_per_function(s) + r3 = check_instance_total(s) + r4 = check_config_modification(s) + + if not r1 or not r2 or not r3 or not r4: + return False + + return True + +def check_file(s: str) -> bool: + if not check_viability(s): + return False + return True + +def check_files(l: list) -> int: + check_list = l[:] + res = 0 + for s in check_list: + if not check_file(s): + res = 1 + return res + +# Status + +def show_diff(l: list): + print(conf_dir) + (_, _, filenames) = next(iter(os.walk(conf_dir))) + filenames.sort() + res = [i for i in filenames if i not in l] + print(res) + +# Read configd-include.json and add/remove/check/show scripts + +with open(include_file, 'r') as f: + try: + include_list = json.load(f) + except OSError as e: + print(f"configd include file error: {e}") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"JSON load error: {e}") + sys.exit(1) + +if args['show_diff']: + show_diff(include_list) + sys.exit(0) + +if args['check_file']: + l = args['check_file'] + ret = check_files(l) + if not ret: + print('pass') + sys.exit(ret) + +if args['check_current']: + ret = check_files(include_list) + if not ret: + print('pass') + sys.exit(ret) + +add_list = args['add'] +# drop redundencies +add_list = [i for i in add_list if i not in include_list] +# prune entries that don't pass check +add_list = [i for i in add_list if check_file(i)] + +remove_list = args['remove'] + +if not add_list and not remove_list: + sys.exit(0) + +separator = '.' +backup_file_name = separator.join([include_file, + '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), 'bak']) + +cmd(f'cp -p {include_file} {backup_file_name}') + +if add_list: + include_list.extend(add_list) + include_list.sort() +if remove_list: + include_list = [i for i in include_list if i not in remove_list] + +with open(include_file, 'w') as f: + try: + json.dump(include_list, f, indent=0) + except OSError as e: + print(f"error writing configd include file: {e}") + sys.exit(1) diff --git a/smoketest/bin/vyos-smoketest b/smoketest/bin/vyos-smoketest new file mode 100755 index 000000000..cb039db42 --- /dev/null +++ b/smoketest/bin/vyos-smoketest @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from stat import S_IXOTH +from subprocess import Popen, PIPE + +success = True +for root, dirs, files in os.walk('/usr/libexec/vyos/tests/smoke'): + for name in files: + test_file = os.path.join(root, name) + mode = os.stat(test_file).st_mode + + if mode & S_IXOTH: + print('Running Testcase: ' + test_file) + process = Popen([test_file], stdout=PIPE) + (output, err) = process.communicate() + exit_code = process.wait() + # We do not want an instant fail - other tests should be run, too + if exit_code != 0: + success = False + +if success: + exit(0) + +print("ERROR: One or more tests failed!") +exit(1) diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py new file mode 100644 index 000000000..14ec7e137 --- /dev/null +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -0,0 +1,269 @@ +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from netifaces import ifaddresses, AF_INET, AF_INET6 + +from vyos.configsession import ConfigSession +from vyos.ifconfig import Interface +from vyos.util import read_file +from vyos.validate import is_intf_addr_assigned, is_ipv6_link_local + +class BasicInterfaceTest: + class BaseTest(unittest.TestCase): + _test_ip = False + _test_mtu = False + _test_vlan = False + _test_qinq = False + _test_ipv6 = False + _base_path = [] + + _options = {} + _interfaces = [] + _qinq_range = ['10', '20', '30'] + _vlan_range = ['100', '200', '300', '2000'] + # choose IPv6 minimum MTU value for tests - this must always work + _mtu = '1280' + + def setUp(self): + self.session = ConfigSession(os.getpid()) + + self._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32', + '2001:db8:1::ffff/64', '2001:db8:101::1/112'] + self._test_mtu = False + self._options = {} + + def tearDown(self): + # we should not remove ethernet from the overall CLI + if 'ethernet' in self._base_path: + for interface in self._interfaces: + # when using a dedicated interface to test via TEST_ETH environment + # variable only this one will be cleared in the end - usable to test + # ethernet interfaces via SSH + self.session.delete(self._base_path + [interface]) + self.session.set(self._base_path + [interface, 'duplex', 'auto']) + self.session.set(self._base_path + [interface, 'speed', 'auto']) + self.session.set(self._base_path + [interface, 'smp-affinity', 'auto']) + else: + self.session.delete(self._base_path) + + self.session.commit() + del self.session + + def test_add_description(self): + """ + Check if description can be added to interface + """ + for intf in self._interfaces: + test_string='Description-Test-{}'.format(intf) + self.session.set(self._base_path + [intf, 'description', test_string]) + for option in self._options.get(intf, []): + self.session.set(self._base_path + [intf] + option.split()) + + self.session.commit() + + # Validate interface description + for intf in self._interfaces: + test_string='Description-Test-{}'.format(intf) + with open('/sys/class/net/{}/ifalias'.format(intf), 'r') as f: + tmp = f.read().rstrip() + self.assertTrue(tmp, test_string) + + def test_add_address_single(self): + """ + Check if a single address can be added to interface. + """ + addr = '192.0.2.0/31' + for intf in self._interfaces: + self.session.set(self._base_path + [intf, 'address', addr]) + for option in self._options.get(intf, []): + self.session.set(self._base_path + [intf] + option.split()) + + self.session.commit() + + for intf in self._interfaces: + self.assertTrue(is_intf_addr_assigned(intf, addr)) + + def test_add_address_multi(self): + """ + Check if IPv4/IPv6 addresses can be added to interface. + """ + + # Add address + for intf in self._interfaces: + for addr in self._test_addr: + self.session.set(self._base_path + [intf, 'address', addr]) + for option in self._options.get(intf, []): + self.session.set(self._base_path + [intf] + option.split()) + + self.session.commit() + + # Validate address + for intf in self._interfaces: + for af in AF_INET, AF_INET6: + for addr in ifaddresses(intf)[af]: + # checking link local addresses makes no sense + if is_ipv6_link_local(addr['addr']): + continue + + self.assertTrue(is_intf_addr_assigned(intf, addr['addr'])) + + def test_ipv6_link_local(self): + """ Common function for IPv6 link-local address assignemnts """ + if not self._test_ipv6: + return None + + for interface in self._interfaces: + base = self._base_path + [interface] + for option in self._options.get(interface, []): + self.session.set(base + option.split()) + + # after commit we must have an IPv6 link-local address + self.session.commit() + + for interface in self._interfaces: + for addr in ifaddresses(interface)[AF_INET6]: + self.assertTrue(is_ipv6_link_local(addr['addr'])) + + # disable IPv6 link-local address assignment + for interface in self._interfaces: + base = self._base_path + [interface] + self.session.set(base + ['ipv6', 'address', 'no-default-link-local']) + + # after commit we must have no IPv6 link-local address + self.session.commit() + + for interface in self._interfaces: + self.assertTrue(AF_INET6 not in ifaddresses(interface)) + + def _mtu_test(self, intf): + """ helper function to verify MTU size """ + with open('/sys/class/net/{}/mtu'.format(intf), 'r') as f: + tmp = f.read().rstrip() + self.assertEqual(tmp, self._mtu) + + def test_change_mtu(self): + """ Testcase if MTU can be changed on interface """ + if not self._test_mtu: + return None + for intf in self._interfaces: + base = self._base_path + [intf] + self.session.set(base + ['mtu', self._mtu]) + for option in self._options.get(intf, []): + self.session.set(base + option.split()) + + self.session.commit() + for intf in self._interfaces: + self._mtu_test(intf) + + def test_8021q_vlan(self): + """ Testcase for 802.1q VLAN interfaces """ + if not self._test_vlan: + return None + + for interface in self._interfaces: + base = self._base_path + [interface] + for option in self._options.get(interface, []): + self.session.set(base + option.split()) + + for vlan in self._vlan_range: + base = self._base_path + [interface, 'vif', vlan] + self.session.set(base + ['mtu', self._mtu]) + for address in self._test_addr: + self.session.set(base + ['address', address]) + + self.session.commit() + for intf in self._interfaces: + for vlan in self._vlan_range: + vif = f'{intf}.{vlan}' + for address in self._test_addr: + self.assertTrue(is_intf_addr_assigned(vif, address)) + self._mtu_test(vif) + + + def test_8021ad_qinq_vlan(self): + """ Testcase for 802.1ad Q-in-Q VLAN interfaces """ + if not self._test_qinq: + return None + + for interface in self._interfaces: + base = self._base_path + [interface] + for option in self._options.get(interface, []): + self.session.set(base + option.split()) + + for vif_s in self._qinq_range: + for vif_c in self._vlan_range: + base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c] + self.session.set(base + ['mtu', self._mtu]) + for address in self._test_addr: + self.session.set(base + ['address', address]) + + self.session.commit() + for interface in self._interfaces: + for vif_s in self._qinq_range: + for vif_c in self._vlan_range: + vif = f'{interface}.{vif_s}.{vif_c}' + for address in self._test_addr: + self.assertTrue(is_intf_addr_assigned(vif, address)) + self._mtu_test(vif) + + def test_ip_options(self): + """ test IP options like arp """ + if not self._test_ip: + return None + + for interface in self._interfaces: + arp_tmo = '300' + path = self._base_path + [interface] + for option in self._options.get(interface, []): + self.session.set(path + option.split()) + + # Options + self.session.set(path + ['ip', 'arp-cache-timeout', arp_tmo]) + self.session.set(path + ['ip', 'disable-arp-filter']) + self.session.set(path + ['ip', 'enable-arp-accept']) + self.session.set(path + ['ip', 'enable-arp-announce']) + self.session.set(path + ['ip', 'enable-arp-ignore']) + self.session.set(path + ['ip', 'enable-proxy-arp']) + self.session.set(path + ['ip', 'proxy-arp-pvlan']) + self.session.set(path + ['ip', 'source-validation', 'loose']) + + self.session.commit() + + for interface in self._interfaces: + tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') + self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/arp_filter') + self.assertEqual('0', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/arp_accept') + self.assertEqual('1', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/arp_announce') + self.assertEqual('1', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/arp_ignore') + self.assertEqual('1', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/proxy_arp') + self.assertEqual('1', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/proxy_arp_pvlan') + self.assertEqual('1', tmp) + + tmp = read_file(f'/proc/sys/net/ipv4/conf/{interface}/rp_filter') + self.assertEqual('2', tmp) diff --git a/smoketest/scripts/cli/test_configd_inspect.py b/smoketest/scripts/cli/test_configd_inspect.py new file mode 100755 index 000000000..5181187e9 --- /dev/null +++ b/smoketest/scripts/cli/test_configd_inspect.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import json +import unittest +import warnings +import importlib.util +from inspect import signature, getsource +from functools import wraps + +from vyos.defaults import directories + +INC_FILE = '/usr/share/vyos/configd-include.json' +CONF_DIR = directories['conf_mode'] + +f_list = ['get_config', 'verify', 'generate', 'apply'] + +def import_script(s): + path = os.path.join(CONF_DIR, s) + name = os.path.splitext(s)[0].replace('-', '_') + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +# importing conf_mode scripts imports jinja2 with deprecation warning +def ignore_deprecation_warning(f): + @wraps(f) + def decorated_function(*args, **kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + f(*args, **kwargs) + return decorated_function + +class TestConfigdInclude(unittest.TestCase): + def setUp(self): + with open(INC_FILE) as f: + self.inc_list = json.load(f) + + @ignore_deprecation_warning + def test_signatures(self): + for s in self.inc_list: + m = import_script(s) + for i in f_list: + f = getattr(m, i, None) + if not f: + continue + sig = signature(f) + par = sig.parameters + l = len(par) + self.assertEqual(l, 1, + f"'{s}': '{i}' incorrect signature") + if i == 'get_config': + for p in par.values(): + self.assertTrue(p.default is None, + f"'{s}': '{i}' incorrect signature") + + @ignore_deprecation_warning + def test_function_instance(self): + for s in self.inc_list: + m = import_script(s) + for i in f_list: + f = getattr(m, i, None) + if not f: + continue + str_f = getsource(f) + n = str_f.count('Config()') + if i == 'get_config': + self.assertEqual(n, 1, + f"'{s}': '{i}' no instance of Config") + if i != 'get_config': + self.assertEqual(n, 0, + f"'{s}': '{i}' instance of Config") + + @ignore_deprecation_warning + def test_file_instance(self): + for s in self.inc_list: + m = import_script(s) + str_m = getsource(m) + n = str_m.count('Config()') + self.assertEqual(n, 1, + f"'{s}' more than one instance of Config") + + @ignore_deprecation_warning + def test_config_modification(self): + for s in self.inc_list: + m = import_script(s) + str_m = getsource(m) + n = str_m.count('my_set') + self.assertEqual(n, 0, f"'{s}' modifies config") + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py new file mode 100755 index 000000000..e3d3b25ee --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from base_interfaces_test import BasicInterfaceTest + +from vyos.ifconfig import Section +from vyos.configsession import ConfigSessionError +from vyos.util import read_file + +class BondingInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._base_path = ['interfaces', 'bonding'] + self._interfaces = ['bond0'] + self._test_mtu = True + self._test_vlan = True + self._test_qinq = True + self._test_ipv6 = True + + self._members = [] + # we need to filter out VLAN interfaces identified by a dot (.) + # in their name - just in case! + if 'TEST_ETH' in os.environ: + self._members = os.environ['TEST_ETH'].split() + else: + for tmp in Section.interfaces("ethernet"): + if not '.' in tmp: + self._members.append(tmp) + + self._options['bond0'] = [] + for member in self._members: + self._options['bond0'].append(f'member interface {member}') + + + def test_add_address_single(self): + """ derived method to check if member interfaces are enslaved properly """ + super().test_add_address_single() + + for interface in self._interfaces: + slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split() + self.assertListEqual(slaves, self._members) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py new file mode 100755 index 000000000..bc0bb69c6 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from base_interfaces_test import BasicInterfaceTest +from vyos.ifconfig import Section + +class BridgeInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._test_ipv6 = True + + self._base_path = ['interfaces', 'bridge'] + self._interfaces = ['br0'] + + self._members = [] + # we need to filter out VLAN interfaces identified by a dot (.) + # in their name - just in case! + if 'TEST_ETH' in os.environ: + self._members = os.environ['TEST_ETH'].split() + else: + for tmp in Section.interfaces("ethernet"): + if not '.' in tmp: + self._members.append(tmp) + + self._options['br0'] = [] + for member in self._members: + self._options['br0'].append(f'member interface {member}') + + def test_add_remove_member(self): + for interface in self._interfaces: + base = self._base_path + [interface] + self.session.set(base + ['stp']) + self.session.set(base + ['address', '192.0.2.1/24']) + + cost = 1000 + priority = 10 + # assign members to bridge interface + for member in self._members: + base_member = base + ['member', 'interface', member] + self.session.set(base_member + ['cost', str(cost)]) + self.session.set(base_member + ['priority', str(priority)]) + cost += 1 + priority += 1 + + self.session.commit() + + for interface in self._interfaces: + self.session.delete(self._base_path + [interface, 'member']) + + self.session.commit() + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py new file mode 100755 index 000000000..01942fc89 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_dummy.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest + +class DummyInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + self._base_path = ['interfaces', 'dummy'] + self._interfaces = ['dum0', 'dum1', 'dum2'] + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py new file mode 100755 index 000000000..761ec7506 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from base_interfaces_test import BasicInterfaceTest +from vyos.ifconfig import Section + +class EthernetInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._base_path = ['interfaces', 'ethernet'] + self._test_ip = True + self._test_mtu = True + self._test_vlan = True + self._test_qinq = True + self._test_ipv6 = True + self._interfaces = [] + + # we need to filter out VLAN interfaces identified by a dot (.) + # in their name - just in case! + if 'TEST_ETH' in os.environ: + tmp = os.environ['TEST_ETH'].split() + self._interfaces = tmp + else: + for tmp in Section.interfaces("ethernet"): + if not '.' in tmp: + self._interfaces.append(tmp) + + def test_dhcp_disable(self): + """ + When interface is configured as admin down, it must be admin down even + """ + for interface in self._interfaces: + self.session.set(self._base_path + [interface, 'disable']) + for option in self._options.get(interface, []): + self.session.set(self._base_path + [interface] + option.split()) + + # Also enable DHCP (ISC DHCP always places interface in admin up + # state so we check that we do not start DHCP client. + # https://phabricator.vyos.net/T2767 + self.session.set(self._base_path + [interface, 'address', 'dhcp']) + + self.session.commit() + + # Validate interface state + for interface in self._interfaces: + with open(f'/sys/class/net/{interface}/flags', 'r') as f: + flags = f.read() + self.assertEqual(int(flags, 16) & 1, 0) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py new file mode 100755 index 000000000..f84a55f86 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_geneve.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from base_interfaces_test import BasicInterfaceTest + + +class GeneveInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._base_path = ['interfaces', 'geneve'] + self._options = { + 'gnv0': ['vni 10', 'remote 127.0.1.1'], + 'gnv1': ['vni 20', 'remote 127.0.1.2'], + } + self._interfaces = list(self._options) + + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py new file mode 100755 index 000000000..d8655d157 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json +import jmespath +import unittest + +from base_interfaces_test import BasicInterfaceTest +from vyos.util import cmd + +class GeneveInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._base_path = ['interfaces', 'l2tpv3'] + self._options = { + 'l2tpeth10': ['local-ip 127.0.0.1', 'remote-ip 127.10.10.10', + 'tunnel-id 100', 'peer-tunnel-id 10', + 'session-id 100', 'peer-session-id 10', + 'source-port 1010', 'destination-port 10101'], + 'l2tpeth20': ['local-ip 127.0.0.1', 'peer-session-id 20', + 'peer-tunnel-id 200', 'remote-ip 127.20.20.20', + 'session-id 20', 'tunnel-id 200', + 'source-port 2020', 'destination-port 20202'], + } + self._interfaces = list(self._options) + + def test_add_address_single(self): + super().test_add_address_single() + + command = 'sudo ip -j l2tp show session' + json_out = json.loads(cmd(command)) + for interface in self._options: + for config in json_out: + if config['interface'] == interface: + # convert list with configuration items into a dict + dict = {} + for opt in self._options[interface]: + dict.update({opt.split()[0].replace('-','_'): opt.split()[1]}) + + for key in ['peer_session_id', 'peer_tunnel_id', 'session_id', 'tunnel_id']: + self.assertEqual(str(config[key]), dict[key]) + + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py new file mode 100755 index 000000000..ba428b5d3 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_loopback.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest +from vyos.validate import is_intf_addr_assigned + +class LoopbackInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + # these addresses are never allowed to be removed from the system + self._loopback_addresses = ['127.0.0.1', '::1'] + self._base_path = ['interfaces', 'loopback'] + self._interfaces = ['lo'] + + def test_add_address_single(self): + super().test_add_address_single() + for addr in self._loopback_addresses: + self.assertTrue(is_intf_addr_assigned('lo', addr)) + + def test_add_address_multi(self): + super().test_add_address_multi() + for addr in self._loopback_addresses: + self.assertTrue(is_intf_addr_assigned('lo', addr)) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py new file mode 100755 index 000000000..0f1b6486d --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import unittest +from psutil import process_iter + +from vyos.ifconfig import Section +from base_interfaces_test import BasicInterfaceTest +from vyos.configsession import ConfigSessionError +from vyos.util import read_file + +def get_config_value(intf, key): + tmp = read_file(f'/run/wpa_supplicant/{intf}.conf') + tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) + return tmp[0] + +class MACsecInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + self._base_path = ['interfaces', 'macsec'] + self._options = { + 'macsec0': ['source-interface eth0', + 'security cipher gcm-aes-128'] + } + + # if we have a physical eth1 interface, add a second macsec instance + if 'eth1' in Section.interfaces("ethernet"): + macsec = { 'macsec1': ['source-interface eth1', 'security cipher gcm-aes-128'] } + self._options.update(macsec) + + self._interfaces = list(self._options) + + def test_encryption(self): + """ MACsec can be operating in authentication and encryption + mode - both using different mandatory settings, lets test + encryption as the basic authentication test has been performed + using the base class tests """ + intf = 'macsec0' + src_intf = 'eth0' + mak_cak = '232e44b7fda6f8e2d88a07bf78a7aff4' + mak_ckn = '40916f4b23e3d548ad27eedd2d10c6f98c2d21684699647d63d41b500dfe8836' + replay_window = '64' + self.session.set(self._base_path + [intf, 'security', 'encrypt']) + + # check validate() - Cipher suite must be set for MACsec + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [intf, 'security', 'cipher', 'gcm-aes-128']) + + # check validate() - Physical source interface must be set for MACsec + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [intf, 'source-interface', src_intf]) + + # check validate() - MACsec security keys mandartory when encryption is enabled + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [intf, 'security', 'mka', 'cak', mak_cak]) + + # check validate() - MACsec security keys mandartory when encryption is enabled + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [intf, 'security', 'mka', 'ckn', mak_ckn]) + + self.session.set(self._base_path + [intf, 'security', 'replay-window', replay_window]) + self.session.commit() + + tmp = get_config_value(src_intf, 'macsec_integ_only') + self.assertTrue("0" in tmp) + + tmp = get_config_value(src_intf, 'mka_cak') + self.assertTrue(mak_cak in tmp) + + tmp = get_config_value(src_intf, 'mka_ckn') + self.assertTrue(mak_ckn in tmp) + + # check that the default priority of 255 is programmed + tmp = get_config_value(src_intf, 'mka_priority') + self.assertTrue("255" in tmp) + + tmp = get_config_value(src_intf, 'macsec_replay_window') + self.assertTrue(replay_window in tmp) + + # Check for running process + self.assertTrue("wpa_supplicant" in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py new file mode 100755 index 000000000..822f05de6 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +config_file = '/etc/ppp/peers/{}' +dhcp6c_config_file = '/run/dhcp6c/dhcp6c.{}.conf' +base_path = ['interfaces', 'pppoe'] + +def get_config_value(interface, key): + with open(config_file.format(interface), 'r') as f: + for line in f: + if line.startswith(key): + return list(line.split()) + return [] + +def get_dhcp6c_config_value(interface, key): + tmp = read_file(dhcp6c_config_file.format(interface)) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + + out = [] + for item in tmp: + out.append(item.replace(';','')) + return out + +class PPPoEInterfaceTest(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self._interfaces = ['pppoe0', 'pppoe50'] + self._source_interface = 'eth0' + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_pppoe(self): + """ Check if PPPoE dialer can be configured and runs """ + for interface in self._interfaces: + user = 'VyOS-user-' + interface + passwd = 'VyOS-passwd-' + interface + mtu = '1400' + + self.session.set(base_path + [interface, 'authentication', 'user', user]) + self.session.set(base_path + [interface, 'authentication', 'password', passwd]) + self.session.set(base_path + [interface, 'default-route', 'auto']) + self.session.set(base_path + [interface, 'mtu', mtu]) + self.session.set(base_path + [interface, 'no-peer-dns']) + + # check validate() - a source-interface is required + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(base_path + [interface, 'source-interface', self._source_interface]) + + # commit changes + self.session.commit() + + # verify configuration file(s) + for interface in self._interfaces: + user = 'VyOS-user-' + interface + password = 'VyOS-passwd-' + interface + + tmp = get_config_value(interface, 'mtu')[1] + self.assertEqual(tmp, mtu) + tmp = get_config_value(interface, 'user')[1].replace('"', '') + self.assertEqual(tmp, user) + tmp = get_config_value(interface, 'password')[1].replace('"', '') + self.assertEqual(tmp, password) + tmp = get_config_value(interface, 'ifname')[1] + self.assertEqual(tmp, interface) + + # Check if ppp process is running in the interface in question + running = False + for p in process_iter(): + if "pppd" in p.name(): + if interface in p.cmdline(): + running = True + + self.assertTrue(running) + + def test_pppoe_dhcpv6pd(self): + """ Check if PPPoE dialer can be configured with DHCPv6-PD """ + address = '1' + sla_id = '0' + sla_len = '8' + for interface in self._interfaces: + self.session.set(base_path + [interface, 'authentication', 'user', 'vyos']) + self.session.set(base_path + [interface, 'authentication', 'password', 'vyos']) + self.session.set(base_path + [interface, 'default-route', 'none']) + self.session.set(base_path + [interface, 'no-peer-dns']) + self.session.set(base_path + [interface, 'source-interface', self._source_interface]) + self.session.set(base_path + [interface, 'ipv6', 'enable']) + + # prefix delegation stuff + dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0'] + self.session.set(dhcpv6_pd_base + ['length', '56']) + self.session.set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) + self.session.set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) + + # commit changes + self.session.commit() + + # verify "normal" PPPoE value - 1492 is default MTU + tmp = get_config_value(interface, 'mtu')[1] + self.assertEqual(tmp, '1492') + tmp = get_config_value(interface, 'user')[1].replace('"', '') + self.assertEqual(tmp, 'vyos') + tmp = get_config_value(interface, 'password')[1].replace('"', '') + self.assertEqual(tmp, 'vyos') + + for param in ['+ipv6', 'ipv6cp-use-ipaddr']: + tmp = get_config_value(interface, param)[0] + self.assertEqual(tmp, param) + + # verify DHCPv6 prefix delegation + # will return: ['delegation', '::/56 infinity;'] + tmp = get_dhcp6c_config_value(interface, 'prefix')[1].split()[0] # mind the whitespace + self.assertEqual(tmp, '::/56') + tmp = get_dhcp6c_config_value(interface, 'prefix-interface')[0].split()[0] + self.assertEqual(tmp, self._source_interface) + tmp = get_dhcp6c_config_value(interface, 'ifid')[0] + self.assertEqual(tmp, address) + tmp = get_dhcp6c_config_value(interface, 'sla-id')[0] + self.assertEqual(tmp, sla_id) + tmp = get_dhcp6c_config_value(interface, 'sla-len')[0] + self.assertEqual(tmp, sla_len) + + # Check if ppp process is running in the interface in question + running = False + for p in process_iter(): + if "pppd" in p.name(): + running = True + self.assertTrue(running) + + # We can not check if wide-dhcpv6 process is running as it is started + # after the PPP interface gets a link to the ISP - but we can see if + # it would be started by the scripts + tmp = read_file(f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{interface}') + tmp = re.findall(f'systemctl start dhcp6c@{interface}.service', tmp) + self.assertTrue(tmp) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py new file mode 100755 index 000000000..bc2e6e7eb --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest + +class PEthInterfaceTest(BasicInterfaceTest.BaseTest): + + def setUp(self): + super().setUp() + self._base_path = ['interfaces', 'pseudo-ethernet'] + + self._test_ip = True + self._test_mtu = True + self._test_vlan = True + self._test_qinq = True + + self._options = { + 'peth0': ['source-interface eth1'], + 'peth1': ['source-interface eth1'], + } + self._interfaces = list(self._options) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py new file mode 100755 index 000000000..7611ffe26 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from vyos.configsession import ConfigSession + +from base_interfaces_test import BasicInterfaceTest + +class TunnelInterfaceTest(BasicInterfaceTest.BaseTest): + # encoding, tunnel endpoint (v4/v6), address (v4/v6) + _valid = [ + ('gre', 4, 4), + ('gre', 4, 6), + ('ip6gre', 6, 4), + ('ip6gre', 6, 6), + ('gre-bridge', 4, 4), + ('ipip', 4, 4), + ('ipip', 4, 6), + ('ipip6', 6, 4), + ('ipip6', 6, 6), + ('ip6ip6', 6, 6), + ('sit', 4, 6), + ] + + local = { + 4: '10.100.{}.1/24', + 6: '2001:db8:{}::1/64', + } + + remote = { + 4: '192.0.{}.1', + 6: '2002::{}:1', + } + + address = { + 4: '10.100.{}.1/24', + 6: '2001:db8:{}::1/64', + } + + def setUp(self): + local = {} + remote = {} + address = {} + + self._intf_dummy = ['interfaces', 'dummy'] + self._base_path = ['interfaces', 'tunnel'] + self._interfaces = ['tun{}'.format(n) for n in range(len(self._valid))] + + self._test_mtu = True + super().setUp() + + for number in range(len(self._valid)): + dum4 = 'dum4{}'.format(number) + dum6 = 'dum6{}'.format(number) + + ipv4 = self.local[4].format(number) + ipv6 = self.local[6].format(number) + + local.setdefault(4, {})[number] = ipv4 + local.setdefault(6, {})[number] = ipv6 + + ipv4 = self.remote[4].format(number) + ipv6 = self.remote[6].format(number) + + remote.setdefault(4, {})[number] = ipv4 + remote.setdefault(6, {})[number] = ipv6 + + ipv4 = self.address[4].format(number) + ipv6 = self.address[6].format(number) + + address.setdefault(4, {})[number] = ipv4 + address.setdefault(6, {})[number] = ipv6 + + self.session.set(self._intf_dummy + [dum4, 'address', ipv4]) + self.session.set(self._intf_dummy + [dum6, 'address', ipv6]) + self.session.commit() + + for number, (encap, p2p, addr) in enumerate(self._valid): + intf = 'tun%d' % number + tunnel = {} + tunnel['encapsulation'] = encap + tunnel['local-ip'] = local[p2p][number].split('/')[0] + tunnel['remote-ip'] = remote[p2p][number].split('/')[0] + tunnel['address'] = address[addr][number] + for name in tunnel: + self.session.set(self._base_path + [intf, name, tunnel[name]]) + + def tearDown(self): + self.session.delete(self._intf_dummy) + super().tearDown() + + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py new file mode 100755 index 000000000..2628e0285 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from base_interfaces_test import BasicInterfaceTest + +class VXLANInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._test_mtu = True + self._base_path = ['interfaces', 'vxlan'] + self._options = { + 'vxlan0': ['vni 10', 'remote 127.0.0.2'], + 'vxlan1': ['vni 20', 'group 239.1.1.1', 'source-interface eth0'], + } + self._interfaces = list(self._options) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py new file mode 100755 index 000000000..0c32a4696 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from base_interfaces_test import BasicInterfaceTest + +# Generate WireGuard default keypair +if not os.path.isdir('/config/auth/wireguard/default'): + os.system('sudo /usr/libexec/vyos/op_mode/wireguard.py --genkey') + +base_path = ['interfaces', 'wireguard'] + +class WireGuardInterfaceTest(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32', + '2001:db8:1::ffff/64', '2001:db8:101::1/112'] + self._interfaces = ['wg0', 'wg1'] + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_peer_setup(self): + """ + Create WireGuard interfaces with associated peers + """ + for intf in self._interfaces: + peer = 'foo-' + intf + psk = 'u2xdA70hkz0S1CG0dZlOh0aq2orwFXRIVrKo4DCvHgM=' + pubkey = 'n6ZZL7ph/QJUJSUUTyu19c77my1dRCDHkMzFQUO9Z3A=' + + for addr in self._test_addr: + self.session.set(base_path + [intf, 'address', addr]) + + self.session.set(base_path + [intf, 'peer', peer, 'address', '127.0.0.1']) + self.session.set(base_path + [intf, 'peer', peer, 'port', '1337']) + + # Allow different prefixes to traverse the tunnel + allowed_ips = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] + for ip in allowed_ips: + self.session.set(base_path + [intf, 'peer', peer, 'allowed-ips', ip]) + + self.session.set(base_path + [intf, 'peer', peer, 'preshared-key', psk]) + self.session.set(base_path + [intf, 'peer', peer, 'pubkey', pubkey]) + self.session.commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}')) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py new file mode 100755 index 000000000..fae233244 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from base_interfaces_test import BasicInterfaceTest +from psutil import process_iter +from vyos.util import check_kmod + +class WirelessInterfaceTest(BasicInterfaceTest.BaseTest): + def setUp(self): + super().setUp() + + self._base_path = ['interfaces', 'wireless'] + self._options = { + 'wlan0': ['physical-device phy0', 'ssid VyOS-WIFI-0', + 'type station', 'address 192.0.2.1/30'], + 'wlan1': ['physical-device phy0', 'ssid VyOS-WIFI-1', + 'type access-point', 'address 192.0.2.5/30', 'channel 0'], + 'wlan10': ['physical-device phy1', 'ssid VyOS-WIFI-2', + 'type station', 'address 192.0.2.9/30'], + 'wlan11': ['physical-device phy1', 'ssid VyOS-WIFI-3', + 'type access-point', 'address 192.0.2.13/30', 'channel 0'], + } + self._interfaces = list(self._options) + self.session.set(['system', 'wifi-regulatory-domain', 'SE']) + + def test_add_address_single(self): + """ derived method to check if member interfaces are enslaved properly """ + super().test_add_address_single() + + for option, option_value in self._options.items(): + if 'type access-point' in option_value: + # Check for running process + self.assertIn('hostapd', (p.name() for p in process_iter())) + elif 'type station' in option_value: + # Check for running process + self.assertIn('wpa_supplicant', (p.name() for p in process_iter())) + else: + self.assertTrue(False) + +if __name__ == '__main__': + check_kmod('mac80211_hwsim') + unittest.main() diff --git a/smoketest/scripts/cli/test_interfaces_wirelessmodem.py b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py new file mode 100755 index 000000000..40cd03b93 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError + +config_file = '/etc/ppp/peers/{}' +base_path = ['interfaces', 'wirelessmodem'] + +def get_config_value(interface, key): + with open(config_file.format(interface), 'r') as f: + for line in f: + if line.startswith(key): + return list(line.split()) + return [] + +class WWANInterfaceTest(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self._interfaces = ['wlm0', 'wlm1'] + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_wlm_1(self): + for interface in self._interfaces: + self.session.set(base_path + [interface, 'no-peer-dns']) + self.session.set(base_path + [interface, 'ondemand']) + + # check validate() - APN must be configure + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(base_path + [interface, 'apn', 'vyos.net']) + + # check validate() - device must be configure + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(base_path + [interface, 'device', 'ttyS0']) + + # commit changes + self.session.commit() + + # verify configuration file(s) + for interface in self._interfaces: + tmp = get_config_value(interface, 'ifname')[1] + self.assertTrue(interface in tmp) + + tmp = get_config_value(interface, 'demand')[0] + self.assertTrue('demand' in tmp) + + tmp = os.path.isfile(f'/etc/ppp/peers/chat.{interface}') + self.assertTrue(tmp) + + # Check if ppp process is running in the interface in question + running = False + for p in process_iter(): + if "pppd" in p.name(): + if interface in p.cmdline(): + running = True + + self.assertTrue(running) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py new file mode 100755 index 000000000..b06fa239d --- /dev/null +++ b/smoketest/scripts/cli/test_nat.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import jmespath +import json +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import cmd + +base_path = ['nat'] +source_path = base_path + ['source'] + +snat_pattern = 'nftables[?rule].rule[?chain].{chain: chain, comment: comment, address: { network: expr[].match.right.prefix.addr | [0], prefix: expr[].match.right.prefix.len | [0]}}' + +class TestNAT(unittest.TestCase): + def setUp(self): + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session = ConfigSession(os.getpid()) + self.session.delete(base_path) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + + def test_source_nat(self): + """ Configure and validate source NAT rule(s) """ + + network = '192.168.0.0/16' + self.session.set(source_path + ['rule', '1', 'destination', 'address', network]) + self.session.set(source_path + ['rule', '1', 'exclude']) + + # check validate() - outbound-interface must be defined + with self.assertRaises(ConfigSessionError): + self.session.commit() + + self.session.set(source_path + ['rule', '1', 'outbound-interface', 'any']) + self.session.commit() + + tmp = cmd('sudo nft -j list table nat') + nftable_json = json.loads(tmp) + condensed_json = jmespath.search(snat_pattern, nftable_json)[0] + + self.assertEqual(condensed_json['comment'], 'DST-NAT-1') + self.assertEqual(condensed_json['address']['network'], network.split('/')[0]) + self.assertEqual(str(condensed_json['address']['prefix']), network.split('/')[1]) + + + def test_validation(self): + """ T2813: Ensure translation address is specified """ + self.session.set(source_path + ['rule', '100', 'outbound-interface', 'eth0']) + + # check validate() - translation address not specified + with self.assertRaises(ConfigSessionError): + self.session.commit() + + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py new file mode 100755 index 000000000..fe4531c3b --- /dev/null +++ b/smoketest/scripts/cli/test_service_bcast-relay.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError + +base_path = ['service', 'broadcast-relay'] + +class TestServiceBroadcastRelay(unittest.TestCase): + _address1 = '192.0.2.1/24' + _address2 = '192.0.2.1/24' + + def setUp(self): + self.session = ConfigSession(os.getpid()) + self.session.set(['interfaces', 'dummy', 'dum1001', 'address', self._address1]) + self.session.set(['interfaces', 'dummy', 'dum1002', 'address', self._address2]) + self.session.commit() + + def tearDown(self): + self.session.delete(['interfaces', 'dummy', 'dum1001']) + self.session.delete(['interfaces', 'dummy', 'dum1002']) + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_service(self): + """ Check if broadcast relay service can be configured and runs """ + ids = range(1, 5) + for id in ids: + base = base_path + ['id', str(id)] + self.session.set(base + ['description', 'vyos']) + self.session.set(base + ['port', str(10000 + id)]) + + # check validate() - two interfaces must be present + with self.assertRaises(ConfigSessionError): + self.session.commit() + + self.session.set(base + ['interface', 'dum1001']) + self.session.set(base + ['interface', 'dum1002']) + self.session.set(base + ['address', self._address1.split('/')[0]]) + + self.session.commit() + + for id in ids: + # check if process is running + running = False + for p in process_iter(): + if "udp-broadcast-relay" in p.name(): + if p.cmdline()[3] == str(id): + running = True + break + self.assertTrue(running) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py new file mode 100755 index 000000000..be52360ed --- /dev/null +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from getpass import getuser +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +DDCLIENT_CONF = '/run/ddclient/ddclient.conf' +base_path = ['service', 'dns', 'dynamic'] + +def get_config_value(key): + tmp = read_file(DDCLIENT_CONF) + tmp = re.findall(r'\n?{}=+(.*)'.format(key), tmp) + tmp = tmp[0].rstrip(',') + return tmp + +def check_process(): + """ + Check for running process, process name changes dynamically e.g. + "ddclient - sleeping for 270 seconds", thus we need a different approach + """ + running = False + for p in process_iter(): + if "ddclient" in p.name(): + running = True + return running + +class TestServiceDDNS(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete DDNS configuration + self.session.delete(base_path) + self.session.commit() + + del self.session + + def test_service(self): + """ Check individual DDNS service providers """ + ddns = ['interface', 'eth0', 'service'] + services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit'] + + for service in services: + user = 'vyos_user' + password = 'vyos_pass' + zone = 'vyos.io' + self.session.delete(base_path) + self.session.set(base_path + ddns + [service, 'host-name', 'test.ddns.vyos.io']) + self.session.set(base_path + ddns + [service, 'login', user]) + self.session.set(base_path + ddns + [service, 'password', password]) + self.session.set(base_path + ddns + [service, 'zone', zone]) + + # commit changes + if service == 'cloudflare': + self.session.commit() + else: + # zone option only works on cloudflare, an exception is raised + # for all others + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.delete(base_path + ddns + [service, 'zone', 'vyos.io']) + # commit changes again - now it should work + self.session.commit() + + # we can only read the configuration file when we operate as 'root' + if getuser() == 'root': + protocol = get_config_value('protocol') + login = get_config_value('login') + pwd = get_config_value('password') + + # some services need special treatment + protoname = service + if service == 'cloudflare': + tmp = get_config_value('zone') + self.assertTrue(tmp == zone) + elif service == 'afraid': + protoname = 'freedns' + elif service == 'dyndns': + protoname = 'dyndns2' + elif service == 'zoneedit': + protoname = 'zoneedit1' + + self.assertTrue(protocol == protoname) + self.assertTrue(login == user) + self.assertTrue(pwd == "'" + password + "'") + + # Check for running process + self.assertTrue(check_process()) + + + def test_rfc2136(self): + """ Check if DDNS service can be configured and runs """ + ddns = ['interface', 'eth0', 'rfc2136', 'vyos'] + ddns_key_file = '/config/auth/my.key' + + self.session.set(base_path + ddns + ['key', ddns_key_file]) + self.session.set(base_path + ddns + ['record', 'test.ddns.vyos.io']) + self.session.set(base_path + ddns + ['server', 'ns1.vyos.io']) + self.session.set(base_path + ddns + ['ttl', '300']) + self.session.set(base_path + ddns + ['zone', 'vyos.io']) + + # ensure an exception will be raised as no key is present + if os.path.exists(ddns_key_file): + os.unlink(ddns_key_file) + + # check validate() - the key file does not exist yet + with self.assertRaises(ConfigSessionError): + self.session.commit() + + with open(ddns_key_file, 'w') as f: + f.write('S3cretKey') + + # commit changes + self.session.commit() + + # TODO: inspect generated configuration file + + # Check for running process + self.assertTrue(check_process()) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py new file mode 100755 index 000000000..5f073b6d2 --- /dev/null +++ b/smoketest/scripts/cli/test_service_https.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from vyos.configsession import ConfigSession +from vyos.util import run + +base_path = ['service', 'https'] + +class TestHTTPSService(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session.delete(base_path) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + + def test_default(self): + self.session.set(base_path) + self.session.commit() + + ret = run('sudo /usr/sbin/nginx -t') + self.assertEqual(ret, 0) + + def test_server_block(self): + vhost_id = 'example' + address = '0.0.0.0' + port = '8443' + name = 'example.org' + + test_path = base_path + ['virtual-host', vhost_id] + + self.session.set(test_path + ['listen-address', address]) + self.session.set(test_path + ['listen-port', port]) + self.session.set(test_path + ['server-name', name]) + + self.session.commit() + + ret = run('sudo /usr/sbin/nginx -t') + self.assertEqual(ret, 0) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py new file mode 100755 index 000000000..18900b6d2 --- /dev/null +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession + +base_path = ['service', 'mdns', 'repeater'] +intf_base = ['interfaces', 'dummy'] + +class TestServiceMDNSrepeater(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + self.session.delete(base_path) + self.session.delete(intf_base + ['dum10']) + self.session.delete(intf_base + ['dum20']) + self.session.commit() + del self.session + + def test_service(self): + # Service required a configured IP address on the interface + + self.session.set(intf_base + ['dum10', 'address', '192.0.2.1/30']) + self.session.set(intf_base + ['dum20', 'address', '192.0.2.5/30']) + + self.session.set(base_path + ['interface', 'dum10']) + self.session.set(base_path + ['interface', 'dum20']) + self.session.commit() + + # Check for running process + self.assertTrue("mdns-repeater" in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py new file mode 100755 index 000000000..901ca792d --- /dev/null +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from configparser import ConfigParser +from psutil import process_iter +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError + +base_path = ['service', 'pppoe-server'] +local_if = ['interfaces', 'dummy', 'dum667'] +pppoe_conf = '/run/accel-pppd/pppoe.conf' + +ac_name = 'ACN' +subnet = '172.18.0.0/24' +gateway = '192.0.2.1' +nameserver = '9.9.9.9' +mtu = '1492' +interface = 'eth0' + +class TestServicePPPoEServer(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session.delete(base_path) + + def tearDown(self): + self.session.delete(base_path) + self.session.delete(local_if) + self.session.commit() + del self.session + + def verify(self, conf): + # validate some common values in the configuration + for tmp in ['log_syslog', 'pppoe', 'chap-secrets', 'ippool', 'ipv6pool', + 'ipv6_nd', 'ipv6_dhcp', 'auth_mschap_v2', 'auth_mschap_v1', + 'auth_chap_md5', 'auth_pap', 'shaper']: + # Settings without values provide None + self.assertEqual(conf['modules'][tmp], None) + + # check Access Concentrator setting + self.assertTrue(conf['pppoe']['ac-name'] == ac_name) + self.assertTrue(conf['pppoe'].getboolean('verbose')) + self.assertTrue(conf['pppoe']['interface'], interface) + + # check configured subnet + self.assertEqual(conf['ip-pool'][subnet], None) + self.assertEqual(conf['ip-pool']['gw-ip-address'], gateway) + + # check ppp + self.assertTrue(conf['ppp'].getboolean('verbose')) + self.assertTrue(conf['ppp'].getboolean('check-ip')) + self.assertEqual(conf['ppp']['min-mtu'], mtu) + self.assertEqual(conf['ppp']['mtu'], mtu) + self.assertEqual(conf['ppp']['lcp-echo-interval'], '30') + self.assertEqual(conf['ppp']['lcp-echo-timeout'], '0') + self.assertEqual(conf['ppp']['lcp-echo-failure'], '3') + + def basic_config(self): + self.session.set(local_if + ['address', '192.0.2.1/32']) + + self.session.set(base_path + ['access-concentrator', ac_name]) + self.session.set(base_path + ['authentication', 'mode', 'local']) + self.session.set(base_path + ['client-ip-pool', 'subnet', subnet]) + self.session.set(base_path + ['name-server', nameserver]) + self.session.set(base_path + ['interface', interface]) + self.session.set(base_path + ['local-ip', gateway]) + + def test_local_auth(self): + """ Test configuration of local authentication for PPPoE server """ + self.basic_config() + # authentication + self.session.set(base_path + ['authentication', 'local-users', 'username', 'vyos', 'password', 'vyos']) + self.session.set(base_path + ['authentication', 'mode', 'local']) + # other settings + self.session.set(base_path + ['ppp-options', 'ccp']) + self.session.set(base_path + ['ppp-options', 'mppe', 'require']) + self.session.set(base_path + ['limits', 'connection-limit', '20/min']) + + # commit changes + self.session.commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True) + conf.read(pppoe_conf) + + # basic verification + self.verify(conf) + + # check auth + self.assertEqual(conf['chap-secrets']['chap-secrets'], '/run/accel-pppd/pppoe.chap-secrets') + self.assertEqual(conf['chap-secrets']['gw-ip-address'], gateway) + + # check pado + self.assertEqual(conf['ppp']['mppe'], 'require') + self.assertTrue(conf['ppp'].getboolean('ccp')) + + # check other settings + self.assertEqual(conf['connlimit']['limit'], '20/min') + + # Check for running process + self.assertTrue('accel-pppd' in (p.name() for p in process_iter())) + + def test_radius_auth(self): + """ Test configuration of RADIUS authentication for PPPoE server """ + radius_server = '192.0.2.22' + radius_key = 'secretVyOS' + radius_port = '2000' + radius_port_acc = '3000' + + self.basic_config() + self.session.set(base_path + ['authentication', 'radius', 'server', radius_server, 'key', radius_key]) + self.session.set(base_path + ['authentication', 'radius', 'server', radius_server, 'port', radius_port]) + self.session.set(base_path + ['authentication', 'radius', 'server', radius_server, 'acct-port', radius_port_acc]) + self.session.set(base_path + ['authentication', 'mode', 'radius']) + + # commit changes + self.session.commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True) + conf.read(pppoe_conf) + + # basic verification + self.verify(conf) + + # check auth + self.assertTrue(conf['radius'].getboolean('verbose')) + self.assertTrue(conf['radius']['acct-timeout'], '3') + self.assertTrue(conf['radius']['timeout'], '3') + self.assertTrue(conf['radius']['max-try'], '3') + self.assertTrue(conf['radius']['gw-ip-address'], gateway) + + server = conf['radius']['server'].split(',') + self.assertEqual(radius_server, server[0]) + self.assertEqual(radius_key, server[1]) + self.assertEqual(f'auth-port={radius_port}', server[2]) + self.assertEqual(f'acct-port={radius_port_acc}', server[3]) + self.assertEqual(f'req-limit=0', server[4]) + self.assertEqual(f'fail-time=0', server[5]) + + # check defaults + self.assertEqual(conf['ppp']['mppe'], 'prefer') + self.assertFalse(conf['ppp'].getboolean('ccp')) + + # Check for running process + self.assertTrue('accel-pppd' in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py new file mode 100755 index 000000000..ec2110c8a --- /dev/null +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession +from vyos.util import read_file + +RADVD_CONF = '/run/radvd/radvd.conf' + +interface = 'eth1' +base_path = ['service', 'router-advert', 'interface', interface] +address_base = ['interfaces', 'ethernet', interface, 'address'] + +def get_config_value(key): + tmp = read_file(RADVD_CONF) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + return tmp[0].split()[0].replace(';','') + +class TestServiceRADVD(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self.session.set(address_base + ['2001:db8::1/64']) + + def tearDown(self): + self.session.delete(address_base) + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_single(self): + self.session.set(base_path + ['prefix', '::/64', 'no-on-link-flag']) + self.session.set(base_path + ['prefix', '::/64', 'no-autonomous-flag']) + self.session.set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) + self.session.set(base_path + ['dnssl', '2001:db8::1234']) + self.session.set(base_path + ['other-config-flag']) + + # commit changes + self.session.commit() + + # verify values + tmp = get_config_value('interface') + self.assertEqual(tmp, interface) + + tmp = get_config_value('prefix') + self.assertEqual(tmp, '::/64') + + tmp = get_config_value('AdvOtherConfigFlag') + self.assertEqual(tmp, 'on') + + # this is a default value + tmp = get_config_value('AdvRetransTimer') + self.assertEqual(tmp, '0') + + # this is a default value + tmp = get_config_value('AdvCurHopLimit') + self.assertEqual(tmp, '64') + + # this is a default value + tmp = get_config_value('AdvDefaultPreference') + self.assertEqual(tmp, 'medium') + + tmp = get_config_value('AdvAutonomous') + self.assertEqual(tmp, 'off') + + # this is a default value + tmp = get_config_value('AdvValidLifetime') + self.assertEqual(tmp, 'infinity') + + # this is a default value + tmp = get_config_value('AdvPreferredLifetime') + self.assertEqual(tmp, '14400') + + tmp = get_config_value('AdvOnLink') + self.assertEqual(tmp, 'off') + + + + # Check for running process + self.assertTrue('radvd' in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py new file mode 100755 index 000000000..fb5f5393f --- /dev/null +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import unittest + +from vyos.validate import is_ipv4 +from psutil import process_iter + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +SNMPD_CONF = '/etc/snmp/snmpd.conf' +base_path = ['service', 'snmp'] + +def get_config_value(key): + tmp = read_file(SNMPD_CONF) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + return tmp[0] + +class TestSNMPService(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session.delete(base_path) + + def tearDown(self): + del self.session + + def test_snmp(self): + """ Check if SNMP can be configured and service runs """ + clients = ['192.0.2.1', '2001:db8::1'] + networks = ['192.0.2.128/25', '2001:db8:babe::/48'] + listen = ['127.0.0.1', '::1'] + + for auth in ['ro', 'rw']: + community = 'VyOS' + auth + self.session.set(base_path + ['community', community, 'authorization', auth]) + for client in clients: + self.session.set(base_path + ['community', community, 'client', client]) + for network in networks: + self.session.set(base_path + ['community', community, 'network', network]) + + for addr in listen: + self.session.set(base_path + ['listen-address', addr]) + + self.session.set(base_path + ['contact', 'maintainers@vyos.io']) + self.session.set(base_path + ['location', 'qemu']) + + self.session.commit() + + # verify listen address, it will be returned as + # ['unix:/run/snmpd.socket,udp:127.0.0.1:161,udp6:[::1]:161'] + # thus we need to transfor this into a proper list + config = get_config_value('agentaddress') + expected = 'unix:/run/snmpd.socket' + for addr in listen: + if is_ipv4(addr): + expected += ',udp:{}:161'.format(addr) + else: + expected += ',udp6:[{}]:161'.format(addr) + + self.assertTrue(expected in config) + + # Check for running process + self.assertTrue("snmpd" in (p.name() for p in process_iter())) + + + def test_snmpv3_sha(self): + """ Check if SNMPv3 can be configured with SHA authentication and service runs""" + + self.session.set(base_path + ['v3', 'engineid', '000000000000000000000002']) + self.session.set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) + # check validate() - a view must be created before this can be comitted + with self.assertRaises(ConfigSessionError): + self.session.commit() + + self.session.set(base_path + ['v3', 'view', 'default', 'oid', '1']) + self.session.set(base_path + ['v3', 'group', 'default', 'view', 'default']) + + # create user + self.session.set(base_path + ['v3', 'user', 'vyos', 'auth', 'plaintext-password', 'vyos12345678']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'auth', 'type', 'sha']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'privacy', 'plaintext-password', 'vyos12345678']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'privacy', 'type', 'aes']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'group', 'default']) + + self.session.commit() + + # commit will alter the CLI values - check if they have been updated: + hashed_password = '4e52fe55fd011c9c51ae2c65f4b78ca93dcafdfe' + tmp = self.session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] + self.assertEqual(tmp, hashed_password) + + tmp = self.session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] + self.assertEqual(tmp, hashed_password) + + # TODO: read in config file and check values + + # Check for running process + self.assertTrue("snmpd" in (p.name() for p in process_iter())) + + def test_snmpv3_md5(self): + """ Check if SNMPv3 can be configured with MD5 authentication and service runs""" + + self.session.set(base_path + ['v3', 'engineid', '000000000000000000000002']) + self.session.set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) + # check validate() - a view must be created before this can be comitted + with self.assertRaises(ConfigSessionError): + self.session.commit() + + self.session.set(base_path + ['v3', 'view', 'default', 'oid', '1']) + self.session.set(base_path + ['v3', 'group', 'default', 'view', 'default']) + + # create user + self.session.set(base_path + ['v3', 'user', 'vyos', 'auth', 'plaintext-password', 'vyos12345678']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'auth', 'type', 'md5']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'privacy', 'plaintext-password', 'vyos12345678']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'privacy', 'type', 'des']) + self.session.set(base_path + ['v3', 'user', 'vyos', 'group', 'default']) + + self.session.commit() + + # commit will alter the CLI values - check if they have been updated: + hashed_password = '4c67690d45d3dfcd33d0d7e308e370ad' + tmp = self.session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] + self.assertEqual(tmp, hashed_password) + + tmp = self.session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] + self.assertEqual(tmp, hashed_password) + + # TODO: read in config file and check values + + # Check for running process + self.assertTrue("snmpd" in (p.name() for p in process_iter())) + + +if __name__ == '__main__': + unittest.main() + diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py new file mode 100755 index 000000000..1038b8775 --- /dev/null +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +SSHD_CONF = '/run/ssh/sshd_config' +base_path = ['service', 'ssh'] + +def get_config_value(key): + tmp = read_file(SSHD_CONF) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + return tmp + +def is_service_running(): + return 'sshd' in (p.name() for p in process_iter()) + +class TestServiceSSH(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session.delete(base_path) + + def tearDown(self): + # delete testing SSH config + self.session.delete(base_path) + # restore "plain" SSH access + self.session.set(base_path) + + self.session.commit() + del self.session + + def test_ssh_default(self): + """ Check if SSH service runs with default settings - used for checking + behavior of <defaultValue> in XML definition """ + self.session.set(base_path) + + # commit changes + self.session.commit() + + # Check configured port + port = get_config_value('Port')[0] + self.assertEqual('22', port) + + # Check for running process + self.assertTrue(is_service_running()) + + def test_ssh_single(self): + """ Check if SSH service can be configured and runs """ + self.session.set(base_path + ['port', '1234']) + self.session.set(base_path + ['disable-host-validation']) + self.session.set(base_path + ['disable-password-authentication']) + self.session.set(base_path + ['loglevel', 'verbose']) + self.session.set(base_path + ['client-keepalive-interval', '100']) + self.session.set(base_path + ['listen-address', '127.0.0.1']) + + # commit changes + self.session.commit() + + # Check configured port + port = get_config_value('Port')[0] + self.assertTrue("1234" in port) + + # Check DNS usage + dns = get_config_value('UseDNS')[0] + self.assertTrue("no" in dns) + + # Check PasswordAuthentication + pwd = get_config_value('PasswordAuthentication')[0] + self.assertTrue("no" in pwd) + + # Check loglevel + loglevel = get_config_value('LogLevel')[0] + self.assertTrue("VERBOSE" in loglevel) + + # Check listen address + address = get_config_value('ListenAddress')[0] + self.assertTrue("127.0.0.1" in address) + + # Check keepalive + keepalive = get_config_value('ClientAliveInterval')[0] + self.assertTrue("100" in keepalive) + + # Check for running process + self.assertTrue(is_service_running()) + + def test_ssh_multi(self): + """ Check if SSH service can be configured and runs with multiple + listen ports and listen-addresses """ + ports = ['22', '2222'] + for port in ports: + self.session.set(base_path + ['port', port]) + + addresses = ['127.0.0.1', '::1'] + for address in addresses: + self.session.set(base_path + ['listen-address', address]) + + # commit changes + self.session.commit() + + # Check configured port + tmp = get_config_value('Port') + for port in ports: + self.assertIn(port, tmp) + + # Check listen address + tmp = get_config_value('ListenAddress') + for address in addresses: + self.assertIn(address, tmp) + + # Check for running process + self.assertTrue(is_service_running()) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py new file mode 100755 index 000000000..931a91c53 --- /dev/null +++ b/smoketest/scripts/cli/test_system_lcd.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Francois Mertz fireboxled@gmail.com +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from configparser import ConfigParser +from psutil import process_iter +from vyos.configsession import ConfigSession + +base_path = ['system', 'lcd'] + +class TestSystemLCD(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_system_display(self): + # configure some system display + self.session.set(base_path + ['device', 'ttyS1']) + self.session.set(base_path + ['model', 'cfa-533']) + + # commit changes + self.session.commit() + + # load up ini-styled LCDd.conf + conf = ConfigParser() + conf.read('/run/LCDd/LCDd.conf') + + self.assertEqual(conf['CFontzPacket']['Model'], '533') + self.assertEqual(conf['CFontzPacket']['Device'], '/dev/ttyS1') + + # both processes running + self.assertTrue('LCDd' in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py new file mode 100755 index 000000000..3c4b1fa28 --- /dev/null +++ b/smoketest/scripts/cli/test_system_login.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import unittest + +from subprocess import Popen, PIPE +from vyos.configsession import ConfigSession, ConfigSessionError +import vyos.util as util + +base_path = ['system', 'login'] +users = ['vyos1', 'vyos2'] + +class TestSystemLogin(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete individual users from configuration + for user in users: + self.session.delete(base_path + ['user', user]) + + self.session.commit() + del self.session + + def test_user(self): + """ Check if user can be created and we can SSH to localhost """ + self.session.set(['service', 'ssh', 'port', '22']) + + for user in users: + name = "VyOS Roxx " + user + home_dir = "/tmp/" + user + + self.session.set(base_path + ['user', user, 'authentication', 'plaintext-password', user]) + self.session.set(base_path + ['user', user, 'full-name', 'VyOS Roxx']) + self.session.set(base_path + ['user', user, 'home-directory', home_dir]) + + self.session.commit() + + for user in users: + cmd = ['su','-', user] + proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + tmp = "{}\nuname -a".format(user) + proc.stdin.write(tmp.encode()) + proc.stdin.flush() + (stdout, stderr) = proc.communicate() + + # stdout is something like this: + # b'Linux vyos 4.19.101-amd64-vyos #1 SMP Sun Feb 2 10:18:07 UTC 2020 x86_64 GNU/Linux\n' + self.assertTrue(len(stdout) > 40) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py new file mode 100755 index 000000000..9040be072 --- /dev/null +++ b/smoketest/scripts/cli/test_system_nameserver.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +import vyos.util as util + +RESOLV_CONF = '/etc/resolv.conf' + +test_servers = ['192.0.2.10', '2001:db8:1::100'] +base_path = ['system', 'name-server'] + +def get_name_servers(): + resolv_conf = util.read_file(RESOLV_CONF) + return re.findall(r'\n?nameserver\s+(.*)', resolv_conf) + +class TestSystemNameServer(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete existing name servers + self.session.delete(base_path) + self.session.commit() + + del self.session + + def test_add_server(self): + """ Check if server is added to resolv.conf """ + for s in test_servers: + self.session.set(base_path + [s]) + self.session.commit() + + servers = get_name_servers() + for s in servers: + self.assertTrue(s in servers) + + def test_delete_server(self): + """ Test if a deleted server disappears from resolv.conf """ + for s in test_servers: + self.session.delete(base_path + [s]) + self.session.commit() + + servers = get_name_servers() + for s in servers: + self.assertTrue(test_server_1 not in servers) + +if __name__ == '__main__': + unittest.main() + diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py new file mode 100755 index 000000000..856a28916 --- /dev/null +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.template import vyos_address_from_cidr, vyos_netmask_from_cidr +from vyos.util import read_file + +NTP_CONF = '/etc/ntp.conf' +base_path = ['system', 'ntp'] + +def get_config_value(key): + tmp = read_file(NTP_CONF) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + # remove possible trailing whitespaces + return [item.strip() for item in tmp] + +class TestSystemNTP(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + self.session.delete(base_path) + + def tearDown(self): + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_ntp_options(self): + """ Test basic NTP support with multiple servers and their options """ + servers = ['192.0.2.1', '192.0.2.2'] + options = ['noselect', 'preempt', 'prefer'] + + for server in servers: + for option in options: + self.session.set(base_path + ['server', server, option]) + + # commit changes + self.session.commit() + + # Check generated configuration + tmp = get_config_value('server') + for server in servers: + test = f'{server} iburst ' + ' '.join(options) + self.assertTrue(test in tmp) + + # Check for running process + self.assertTrue("ntpd" in (p.name() for p in process_iter())) + + def test_ntp_clients(self): + """ Test the allowed-networks statement """ + listen_address = ['127.0.0.1', '::1'] + for listen in listen_address: + self.session.set(base_path + ['listen-address', listen]) + + networks = ['192.0.2.0/24', '2001:db8:1000::/64'] + for network in networks: + self.session.set(base_path + ['allow-clients', 'address', network]) + + # Verify "NTP server not configured" verify() statement + with self.assertRaises(ConfigSessionError): + self.session.commit() + + servers = ['192.0.2.1', '192.0.2.2'] + for server in servers: + self.session.set(base_path + ['server', server]) + + self.session.commit() + + # Check generated client address configuration + for network in networks: + network_address = vyos_address_from_cidr(network) + network_netmask = vyos_netmask_from_cidr(network) + + tmp = get_config_value(f'restrict {network_address}')[0] + test = f'mask {network_netmask} nomodify notrap nopeer' + self.assertTrue(tmp in test) + + # Check listen address + tmp = get_config_value('interface') + test = ['ignore wildcard'] + for listen in listen_address: + test.append(f'listen {listen}') + self.assertEqual(tmp, test) + + # Check for running process + self.assertTrue("ntpd" in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py new file mode 100755 index 000000000..d2b82d686 --- /dev/null +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import unittest + +from psutil import process_iter +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +OCSERV_CONF = '/run/ocserv/ocserv.conf' +base_path = ['vpn', 'openconnect'] +cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' +cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' + +class TestVpnOpenconnect(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete vpn openconnect configuration + self.session.delete(base_path) + self.session.commit() + + del self.session + + def test_vpn(self): + user = 'vyos_user' + password = 'vyos_pass' + self.session.delete(base_path) + self.session.set(base_path + ["authentication", "local-users", "username", user, "password", password]) + self.session.set(base_path + ["authentication", "mode", "local"]) + self.session.set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) + self.session.set(base_path + ["ssl", "ca-cert-file", cert]) + self.session.set(base_path + ["ssl", "cert-file", cert]) + self.session.set(base_path + ["ssl", "key-file", cert_key]) + + self.session.commit() + + # Check for running process + self.assertTrue("ocserv-main" in (p.name() for p in process_iter())) + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py new file mode 100755 index 000000000..efa095b30 --- /dev/null +++ b/smoketest/scripts/cli/test_vrf.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file + +class VRFTest(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self._vrfs = ['red', 'green', 'blue'] + + def tearDown(self): + # delete all VRFs + self.session.delete(['vrf']) + self.session.commit() + del self.session + + def test_table_id(self): + table = 1000 + for vrf in self._vrfs: + base = ['vrf', 'name', vrf] + description = "VyOS-VRF-" + vrf + self.session.set(base + ['description', description]) + + # check validate() - a table ID is mandatory + with self.assertRaises(ConfigSessionError): + self.session.commit() + + self.session.set(base + ['table', str(table)]) + table += 1 + + # commit changes + self.session.commit() + +if __name__ == '__main__': + unittest.main() diff --git a/smoketest/scripts/system/test_module_load.py b/smoketest/scripts/system/test_module_load.py new file mode 100755 index 000000000..c3cf0ff92 --- /dev/null +++ b/smoketest/scripts/system/test_module_load.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import unittest + +modules = { + "intel": ["e1000", "e1000e", "igb", "ixgb", "ixgbe", "ixgbevf", "i40e", "i40evf", "iavf"], + "intel_qat": ["qat_c3xxx", "qat_c3xxxvf", "qat_c62x", "qat_c62xvf", "qat_d15xx", "qat_d15xxvf", "qat_dh895xcc", "qat_dh895xccvf", "usdm_drv"], + "accel_ppp": ["ipoe", "vlan_mon"], + "misc": ["wireguard"] +} + +class TestKernelModules(unittest.TestCase): + def test_load_modules(self): + success = True + for msk in modules: + ms = modules[msk] + for m in ms: + # We want to uncover all modules that fail, + # not fail at the first one + try: + os.system("modprobe {0}".format(m)) + except: + success = False + + self.assertTrue(success) + +if __name__ == '__main__': + unittest.main() diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 5c7294296..4a47b9246 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -15,151 +15,87 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import fnmatch +from glob import glob +from netifaces import interfaces from sys import exit -from copy import deepcopy from vyos.config import Config -from vyos import ConfigError from vyos.util import call from vyos.template import render - +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/etc/default/udp-broadcast-relay' - -default_config_data = { - 'disabled': False, - 'instances': [] -} - -def get_config(): - relay = deepcopy(default_config_data) - conf = Config() - base = ['service', 'broadcast-relay'] +config_file_base = r'/etc/default/udp-broadcast-relay' - if not conf.exists(base): - return None +def get_config(config=None): + if config: + conf = config else: - conf.set_level(base) - - # Service can be disabled by user - if conf.exists('disable'): - relay['disabled'] = True - return relay - - # Parse configuration of each individual instance - if conf.exists('id'): - for id in conf.list_nodes('id'): - conf.set_level(base + ['id', id]) - config = { - 'id': id, - 'disabled': False, - 'address': '', - 'description': '', - 'interfaces': [], - 'port': '' - } - - # Check if individual broadcast relay service is disabled - if conf.exists(['disable']): - config['disabled'] = True - - # Source IP of forwarded packets, if empty original senders address is used - if conf.exists(['address']): - config['address'] = conf.return_value(['address']) - - # A description for each individual broadcast relay service - if conf.exists(['description']): - config['description'] = conf.return_value(['description']) - - # UDP port to listen on for broadcast frames - if conf.exists(['port']): - config['port'] = conf.return_value(['port']) - - # Network interfaces to listen on for broadcast frames to be relayed - if conf.exists(['interface']): - config['interfaces'] = conf.return_values(['interface']) - - relay['instances'].append(config) + conf = Config() + base = ['service', 'broadcast-relay'] + relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return relay def verify(relay): - if relay is None: - return None - - if relay['disabled']: + if not relay or 'disabled' in relay: return None - for r in relay['instances']: + for instance, config in relay.get('id', {}).items(): # we don't have to check this instance when it's disabled - if r['disabled']: + if 'disabled' in config: continue # we certainly require a UDP port to listen to - if not r['port']: - raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id'])) + if 'port' not in config: + raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"') + # if only oone interface is given it's a string -> move to list + if isinstance(config.get('interface', []), str): + config['interface'] = [ config['interface'] ] # Relaying data without two interface is kinda senseless ... - if len(r['interfaces']) < 2: - raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id'])) + if len(config.get('interface', [])) < 2: + raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"') - return None + for interface in config.get('interface', []): + if interface not in interfaces(): + raise ConfigError('Interface "{interface}" does not exist!') + return None def generate(relay): - if relay is None: + if not relay or 'disabled' in relay: return None - 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() - - # delete old configuration files - for id in active_configs[:]: - if os.path.exists(config_file + id): - os.unlink(config_file + id) + for config in glob(config_file_base + '*'): + os.remove(config) - # If the service is disabled, we can bail out here - if relay['disabled']: - print('Warning: UDP broadcast relay service will be deactivated because it is disabled') - return None - - for r in relay['instances']: - # Skip writing instance config when it's disabled - if r['disabled']: + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: continue - # configuration filename contains instance id - file = config_file + str(r['id']) - render(file, 'bcast-relay/udp-broadcast-relay.tmpl', r) + config['instance'] = instance + render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', config) return None def apply(relay): # first stop all running services - call('systemctl stop udp-broadcast-relay@{1..99}.service') + call('systemctl stop udp-broadcast-relay@*.service') - if (relay is None) or relay['disabled']: + if not relay or 'disable' in relay: return None # start only required service instances - for r in relay['instances']: - # Don't start individual instance when it's disabled - if r['disabled']: + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: continue - call('systemctl start udp-broadcast-relay@{0}.service'.format(r['id'])) + + call(f'systemctl start udp-broadcast-relay@{instance}.service') return None diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index f093a005e..352865b9d 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -36,9 +36,12 @@ default_config_data = { 'relay_agent_packets': 'forward' } -def get_config(): +def get_config(config=None): relay = default_config_data - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists(['service', 'dhcp-relay']): return None else: diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 0eaa14c5b..fd4e2ec61 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -126,9 +126,12 @@ def dhcp_static_route(static_subnet, static_router): return string -def get_config(): +def get_config(config=None): dhcp = default_config_data - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists('service dhcp-server'): return None else: diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index 6ef290bf0..d4212b8be 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -35,9 +35,12 @@ default_config_data = { 'options': [], } -def get_config(): +def get_config(config=None): relay = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists('service dhcpv6-relay'): return None else: diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 53c8358a5..4ce4cada1 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -37,9 +37,12 @@ default_config_data = { 'shared_network': [] } -def get_config(): +def get_config(config=None): dhcpv6 = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base = ['service', 'dhcpv6-server'] if not conf.exists(base): return None diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 5b1883c03..57c910a68 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -50,9 +50,12 @@ default_config_data = { 'deleted': False } -def get_config(): +def get_config(config=None): dyndns = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base_level = ['service', 'dns', 'dynamic'] if not conf.exists(base_level): diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index 71b2a98b3..67bf5d0e2 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -32,9 +32,12 @@ default_config_data = { 'new_chain6': False } -def get_config(): +def get_config(config=None): opts = copy.deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists('firewall options'): # bail out early return opts diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index f2fa64233..f4c75c257 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -18,20 +18,16 @@ conf-mode script for 'system host-name' and 'system domain-name'. """ -import os import re import sys import copy -import glob -import argparse -import jinja2 import vyos.util import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, call, run, process_named_running +from vyos.util import cmd, call, process_named_running from vyos import airbag airbag.enable() @@ -47,7 +43,12 @@ default_config_data = { hostsd_tag = 'system' -def get_config(conf): +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + hosts = copy.deepcopy(default_config_data) hosts['hostname'] = conf.return_value("system host-name") @@ -77,7 +78,7 @@ def get_config(conf): return hosts -def verify(conf, hosts): +def verify(hosts): if hosts is None: return None @@ -168,9 +169,8 @@ def apply(config): if __name__ == '__main__': try: - conf = Config() - c = get_config(conf) - verify(conf, c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index b8a084a40..472eb77e4 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -39,7 +39,7 @@ dependencies = [ 'https.py', ] -def get_config(): +def get_config(config=None): http_api = deepcopy(vyos.defaults.api_data) x = http_api.get('api_keys') if x is None: @@ -48,7 +48,11 @@ def get_config(): default_key = x[0] keys_added = False - conf = Config() + if config: + conf = config + else: + conf = Config() + if not conf.exists('service https api'): return None else: diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 7acb629bd..de228f0f8 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -14,9 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os +import sys -from sys import exit from copy import deepcopy import vyos.defaults @@ -31,7 +30,13 @@ from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' +certbot_dir = vyos.defaults.directories['certbot'] +# https config needs to coordinate several subsystems: api, certbot, +# self-signed certificate, as well as the virtual hosts defined within the +# https config definition itself. Consequently, one needs a general dict, +# encompassing the https and other configs, and a list of such virtual hosts +# (server blocks in nginx terminology) to pass to the jinja2 template. default_server_block = { 'id' : '', 'address' : '*', @@ -42,70 +47,84 @@ default_server_block = { 'certbot' : False } -def get_config(): - server_block_list = [] - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + if not conf.exists('service https'): return None - else: - conf.set_level('service https') - if not conf.exists('virtual-host'): + server_block_list = [] + https_dict = conf.get_config_dict('service https', get_first_key=True) + + # organize by vhosts + + vhost_dict = https_dict.get('virtual-host', {}) + + if not vhost_dict: + # no specified virtual hosts (server blocks); use default server_block_list.append(default_server_block) else: - for vhost in conf.list_nodes('virtual-host'): + for vhost in list(vhost_dict): server_block = deepcopy(default_server_block) server_block['id'] = vhost - if conf.exists(f'virtual-host {vhost} listen-address'): - addr = conf.return_value(f'virtual-host {vhost} listen-address') - server_block['address'] = addr - if conf.exists(f'virtual-host {vhost} listen-port'): - port = conf.return_value(f'virtual-host {vhost} listen-port') - server_block['port'] = port - if conf.exists(f'virtual-host {vhost} server-name'): - names = conf.return_values(f'virtual-host {vhost} server-name') - server_block['name'] = names[:] + data = vhost_dict.get(vhost, {}) + server_block['address'] = data.get('listen-address', '*') + server_block['port'] = data.get('listen-port', '443') + name = data.get('server-name', ['_']) + server_block['name'] = name server_block_list.append(server_block) + # get certificate data + + cert_dict = https_dict.get('certificates', {}) + + # self-signed certificate + vyos_cert_data = {} - if conf.exists('certificates system-generated-certificate'): + if 'system-generated-certificate' in list(cert_dict): vyos_cert_data = vyos.defaults.vyos_cert_data if vyos_cert_data: for block in server_block_list: block['vyos_cert'] = vyos_cert_data + # letsencrypt certificate using certbot + certbot = False - certbot_domains = [] - if conf.exists('certificates certbot domain-name'): - certbot_domains = conf.return_values('certificates certbot domain-name') - if certbot_domains: + cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) + if cert_domains: certbot = True - for domain in certbot_domains: + for domain in cert_domains: sub_list = vyos.certbot_util.choose_server_block(server_block_list, domain) if sub_list: for sb in sub_list: sb['certbot'] = True + sb['certbot_dir'] = certbot_dir # certbot organizes certificates by first domain - sb['certbot_dir'] = certbot_domains[0] + sb['certbot_domain_dir'] = cert_domains[0] - api_somewhere = False + # get api data + + api_set = False api_data = {} - if conf.exists('api'): - api_somewhere = True + if 'api' in list(https_dict): + api_set = True api_data = vyos.defaults.api_data - if conf.exists('api port'): - port = conf.return_value('api port') + api_settings = https_dict.get('api', {}) + if api_settings: + port = api_settings.get('port', '') + if port: api_data['port'] = port - if conf.exists('api-restrict virtual-host'): - vhosts = conf.return_values('api-restrict virtual-host') + vhosts = https_dict.get('api-restrict', {}).get('virtual-host', []) + if vhosts: api_data['vhost'] = vhosts[:] if api_data: - # we do not want to include 'vhost' key as part of - # vyos.defaults.api_data, so check for key existence - vhost_list = api_data.get('vhost') - if vhost_list is None: + vhost_list = api_data.get('vhost', []) + if not vhost_list: for block in server_block_list: block['api'] = api_data else: @@ -113,9 +132,12 @@ def get_config(): if block['id'] in vhost_list: block['api'] = api_data + # return dict for use in template + https = {'server_block_list' : server_block_list, - 'api_somewhere': api_somewhere, + 'api_set': api_set, 'certbot': certbot} + return https def verify(https): @@ -155,4 +177,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - exit(1) + sys.exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index 49aea9b7f..754f46566 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -36,9 +36,12 @@ default_config_data = { 'interfaces': [], } -def get_config(): +def get_config(config=None): igmp_proxy = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base = ['protocols', 'igmp-proxy'] if not conf.exists(base): return None diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py index 742f09a54..1e5101a9f 100755 --- a/src/conf_mode/intel_qat.py +++ b/src/conf_mode/intel_qat.py @@ -30,8 +30,11 @@ airbag.enable() # Define for recovering gl_ipsec_conf = None -def get_config(): - c = Config() +def get_config(config=None): + if config: + c = config + else: + c = Config() config_data = { 'qat_conf' : None, 'ipsec_conf' : None, diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index a16c4e105..16e6e4f6e 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -16,41 +16,25 @@ import os -from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data from vyos.config import Config -from vyos.util import call, cmd -from vyos.validate import is_member, has_address_configured +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import BondIf +from vyos.validate import is_member +from vyos.validate import has_address_configured from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'arp_mon_intvl': 0, - 'arp_mon_tgt': [], - 'deleted': False, - 'hash_policy': 'layer2', - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'mode': '802.3ad', - 'member': [], - 'shutdown_required': False, - 'primary': '', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], -} - - def get_bond_mode(mode): if mode == 'round-robin': return 'balance-rr' @@ -67,339 +51,141 @@ def get_bond_mode(mode): elif mode == 'adaptive-load-balance': return 'balance-alb' else: - raise ConfigError('invalid bond mode "{}"'.format(mode)) - -def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - conf = Config() - - # initialize kernel module if not loaded - if not os.path.isfile('/sys/class/net/bonding_masters'): - import syslog - syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if call('modprobe bonding max_bonds=0 miimon=250') != 0: - syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") - raise ConfigError("failed loading bonding kernel module") - - # check if bond has been removed - cfg_base = 'interfaces bonding ' + ifname - if not conf.exists(cfg_base): - bond = deepcopy(default_config_data) - bond['intf'] = ifname - bond['deleted'] = True - return bond - - # set new configuration level - conf.set_level(cfg_base) - - bond, disabled = intf_to_dict(conf, default_config_data) - - # ARP link monitoring frequency in milliseconds - if conf.exists('arp-monitor interval'): - bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) - - # IP address to use for ARP monitoring - if conf.exists('arp-monitor target'): - bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') - - # Bonding transmit hash policy - if conf.exists('hash-policy'): - bond['hash_policy'] = conf.return_value('hash-policy') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - bond['ip_proxy_arp_pvlan'] = 1 - - # Bonding mode - if conf.exists('mode'): - act_mode = conf.return_value('mode') - eff_mode = conf.return_effective_value('mode') - if not (act_mode == eff_mode): - bond['shutdown_required'] = True - - bond['mode'] = get_bond_mode(act_mode) - - # determine bond member interfaces (currently configured) - bond['member'] = conf.return_values('member interface') - - # We can not call conf.return_effective_values() as it would not work - # on reboots. Reboots/First boot will return that running config and - # saved config is the same, thus on a reboot the bond members will - # not be added all (https://phabricator.vyos.net/T2030) - live_members = BondIf(bond['intf']).get_slaves() - if not (bond['member'] == live_members): - bond['shutdown_required'] = True - - # Primary device interface - if conf.exists('primary'): - bond['primary'] = conf.return_value('primary') - - add_to_dict(conf, disabled, bond, 'vif', 'vif') - add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s') + raise ConfigError(f'invalid bond mode "{mode}"') + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'bonding'] + bond = get_interface_dict(conf, base) + + # To make our own life easier transfor the list of member interfaces + # into a dictionary - we will use this to add additional information + # later on for wach member + if 'member' in bond and 'interface' in bond['member']: + # first convert it to a list if only one member is given + if isinstance(bond['member']['interface'], str): + bond['member']['interface'] = [bond['member']['interface']] + + tmp={} + for interface in bond['member']['interface']: + tmp.update({interface: {}}) + + bond['member']['interface'] = tmp + + if 'mode' in bond: + bond['mode'] = get_bond_mode(bond['mode']) + + tmp = leaf_node_changed(conf, ['mode']) + if tmp: + bond.update({'shutdown_required': ''}) + + # determine which members have been removed + tmp = leaf_node_changed(conf, ['member', 'interface']) + if tmp: + bond.update({'shutdown_required': ''}) + if 'member' in bond: + bond['member'].update({'interface_remove': tmp }) + else: + bond.update({'member': {'interface_remove': tmp }}) + + if 'member' in bond and 'interface' in bond['member']: + for interface, interface_config in bond['member']['interface'].items(): + # Check if we are a member of another bond device + tmp = is_member(conf, interface, 'bridge') + if tmp: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp and tmp != bond['ifname']: + interface_config.update({'is_bond_member' : tmp}) + + # bond members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) return bond def verify(bond): - if bond['deleted']: - if bond['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{bond["intf"]}" as it is a ' - f'member of bridge "{bond["is_bridge_member"]}"!')) - + if 'deleted' in bond: + verify_bridge_delete(bond) return None - if len(bond['arp_mon_tgt']) > 16: - raise ConfigError('The maximum number of arp-monitor targets is 16') + if 'arp_monitor' in bond: + if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: + raise ConfigError('The maximum number of arp-monitor targets is 16') + + if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') - if bond['primary']: + if 'primary' in bond: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError(( - 'Mode dependency failed, primary not supported in mode ' - f'"{bond["mode"]}"!')) - - if ( bond['is_bridge_member'] - and ( bond['address'] - or bond['ipv6_eui64_prefix'] - or bond['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{bond["intf"]}" ' - f'as it is a member of bridge "{bond["is_bridge_member"]}"!')) - - if bond['vrf']: - if bond['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{bond["vrf"]}" does not exist') - - if bond['is_bridge_member']: - raise ConfigError(( - f'Interface "{bond["intf"]}" cannot be member of VRF ' - f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} ' - f'at the same time!')) + raise ConfigError('Option primary - mode dependency failed, not' + 'supported in mode {mode}!'.format(**bond)) + + verify_address(bond) + verify_dhcpv6(bond) + verify_vrf(bond) # use common function to verify VLAN configuration verify_vlan_config(bond) - conf = Config() - for intf in bond['member']: - # check if member interface is "real" - if intf not in interfaces(): - raise ConfigError(f'Interface {intf} does not exist!') - - # a bonding member interface is only allowed to be assigned to one bond! - all_bonds = conf.list_nodes('interfaces bonding') - # We do not need to check our own bond - all_bonds.remove(bond['intf']) - for tmp in all_bonds: - if conf.exists('interfaces bonding {tmp} member interface {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of bond "{tmp}"!')) - - # can not add interfaces with an assigned address to a bond - if has_address_configured(conf, intf): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it has an address assigned!')) - - # bond members are not allowed to be bridge members - tmp = is_member(conf, intf, 'bridge') - if tmp: - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of bridge "{tmp}"!')) - - # bond members are not allowed to be vrrp members - for tmp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group {tmp} interface {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already a member of VRRP group "{tmp}"!')) - - # bond members are not allowed to be underlaying psuedo-ethernet devices - for tmp in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already the link of pseudo-ethernet "{tmp}"!')) - - # bond members are not allowed to be underlaying vxlan devices - for tmp in conf.list_nodes('interfaces vxlan'): - if conf.exists('interfaces vxlan {tmp} link {intf}'): - raise ConfigError(( - f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' - f'it is already the link of VXLAN "{tmp}"!')) - - if bond['primary']: - if bond['primary'] not in bond['member']: - raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member') + bond_name = bond['ifname'] + if 'member' in bond: + member = bond.get('member') + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", ' + + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bond') + + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') + + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') + + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') + + + if 'primary' in bond: + if bond['primary'] not in bond['member']['interface']: + raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface') if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('primary interface only works for mode active-backup, ' \ 'transmit-load-balance or adaptive-load-balance') - if bond['arp_mon_intvl'] > 0: - if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: - raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ - 'transmit-load-balance or adaptive-load-balance') - return None def generate(bond): return None def apply(bond): - b = BondIf(bond['intf']) + b = BondIf(bond['ifname']) - if bond['deleted']: + if 'deleted' in bond: # delete interface b.remove() else: - # ARP link monitoring frequency, reset miimon when arp-montior is inactive - # this is done inside BondIf automatically - b.set_arp_interval(bond['arp_mon_intvl']) - - # ARP monitor targets need to be synchronized between sysfs and CLI. - # Unfortunately an address can't be send twice to sysfs as this will - # result in the following exception: OSError: [Errno 22] Invalid argument. - # - # We remove ALL adresses prior adding new ones, this will remove addresses - # added manually by the user too - but as we are limited to 16 adresses - # from the kernel side this looks valid to me. We won't run into an error - # when a user added manual adresses which would result in having more - # then 16 adresses in total. - arp_tgt_addr = list(map(str, b.get_arp_ip_target().split())) - for addr in arp_tgt_addr: - b.set_arp_ip_target('-' + addr) - - # Add configured ARP target addresses - for addr in bond['arp_mon_tgt']: - b.set_arp_ip_target('+' + addr) - - # update interface description used e.g. within SNMP - b.set_alias(bond['description']) - - if bond['dhcp_client_id']: - b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] - - if bond['dhcp_hostname']: - b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] - - if bond['dhcp_vendor_class_id']: - b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] - - if bond['dhcpv6_prm_only']: - b.dhcp.v6.options['dhcpv6_prm_only'] = True - - if bond['dhcpv6_temporary']: - b.dhcp.v6.options['dhcpv6_temporary'] = True - - if bond['dhcpv6_pd_length']: - b.dhcp.v6.options['dhcpv6_pd_length'] = bond['dhcpv6_pd_length'] - - if bond['dhcpv6_pd_interfaces']: - b.dhcp.v6.options['dhcpv6_pd_interfaces'] = bond['dhcpv6_pd_interfaces'] - - # ignore link state changes - b.set_link_detect(bond['disable_link_detect']) - # Bonding transmit hash policy - b.set_hash_policy(bond['hash_policy']) - # configure ARP cache timeout in milliseconds - b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) - # configure ARP filter configuration - b.set_arp_filter(bond['ip_disable_arp_filter']) - # configure ARP accept - b.set_arp_accept(bond['ip_enable_arp_accept']) - # configure ARP announce - b.set_arp_announce(bond['ip_enable_arp_announce']) - # configure ARP ignore - b.set_arp_ignore(bond['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - b.set_proxy_arp(bond['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) - # IPv6 accept RA - b.set_ipv6_accept_ra(bond['ipv6_accept_ra']) - # IPv6 address autoconfiguration - b.set_ipv6_autoconf(bond['ipv6_autoconf']) - # IPv6 forwarding - b.set_ipv6_forwarding(bond['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in bond['ipv6_eui64_prefix_remove']: - b.del_ipv6_eui64_address(addr) - - # Change interface MAC address - if bond['mac']: - b.set_mac(bond['mac']) - - # Add IPv6 EUI-based addresses - for addr in bond['ipv6_eui64_prefix']: - b.add_ipv6_eui64_address(addr) - - # Maximum Transmission Unit (MTU) - b.set_mtu(bond['mtu']) - - # Primary device interface - if bond['primary']: - b.set_primary(bond['primary']) - - # Some parameters can not be changed when the bond is up. - if bond['shutdown_required']: - # Disable bond prior changing of certain properties - b.set_admin_state('down') - - # The bonding mode can not be changed when there are interfaces enslaved - # to this bond, thus we will free all interfaces from the bond first! - for intf in b.get_slaves(): - b.del_port(intf) - - # Bonding policy/mode - b.set_mode(bond['mode']) - - # Add (enslave) interfaces to bond - for intf in bond['member']: - # if we've come here we already verified the interface doesn't - # have addresses configured so just flush any remaining ones - cmd(f'ip addr flush dev "{intf}"') - b.add_port(intf) - - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not bond['disable']: - b.set_admin_state('up') - else: - b.set_admin_state('down') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bond['address_remove']: - b.del_addr(addr) - for addr in bond['address']: - b.add_addr(addr) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not bond['is_bridge_member']: - b.set_vrf(bond['vrf']) - - # re-add ourselves to any bridge we might have fallen out of - if bond['is_bridge_member']: - b.add_to_bridge(bond['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(b, bond) + b.update(bond) return None diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 1e4fa5816..47c8c05f9 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -16,251 +16,105 @@ import os -from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BridgeIf, Section -from vyos.ifconfig.stp import STP -from vyos.configdict import list_diff, interface_default_data -from vyos.validate import is_member, has_address_configured from vyos.config import Config -from vyos.util import cmd, get_bridge_member_config +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_vrf +from vyos.ifconfig import BridgeIf +from vyos.validate import is_member, has_address_configured +from vyos.xml import defaults + +from vyos.util import cmd from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'aging': 300, - 'arp_cache_tmo': 30, - 'deleted': False, - 'forwarding_delay': 14, - 'hello_time': 2, - 'igmp_querier': 0, - 'intf': '', - 'max_age': 20, - 'member': [], - 'member_remove': [], - 'priority': 32768, - 'stp': 0 -} - -def get_config(): - bridge = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # Check if bridge has been removed - if not conf.exists('interfaces bridge ' + bridge['intf']): - bridge['deleted'] = True - return bridge - - # set new configuration level - conf.set_level('interfaces bridge ' + bridge['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - bridge['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - bridge['address_remove'] = list_diff(eff_addr, bridge['address']) - - # retrieve aging - how long addresses are retained - if conf.exists('aging'): - bridge['aging'] = int(conf.return_value('aging')) - - # retrieve interface description - if conf.exists('description'): - bridge['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True - - # Disable this bridge interface - if conf.exists('disable'): - bridge['disable'] = True - - # Ignore link state changes - if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = 2 - - # Forwarding delay - if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) - - # Hello packet advertisment interval - if conf.exists('hello-time'): - bridge['hello_time'] = int(conf.return_value('hello-time')) - - # Enable Internet Group Management Protocol (IGMP) querier - if conf.exists('igmp querier'): - bridge['igmp_querier'] = 1 - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - bridge['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - bridge['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - bridge['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - bridge['ip_enable_arp_ignore'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - bridge['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - - # Determine currently effective EUI64 addresses - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('ipv6 address eui64') - bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix']) - - # Remove the default link-local address if set. - if conf.exists('ipv6 address no-default-link-local'): - bridge['ipv6_eui64_prefix_remove'].append('fe80::/64') +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config else: - # add the link-local by default to make IPv6 work - bridge['ipv6_eui64_prefix'].append('fe80::/64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - bridge['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # Media Access Control (MAC) address - if conf.exists('mac'): - bridge['mac'] = conf.return_value('mac') - - # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses - # before re-adding them - if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge') - and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ): - bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix'] - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']: - bridge['ipv6_accept_ra'] = 2 - - # Interval at which neighbor bridges are removed - if conf.exists('max-age'): - bridge['max_age'] = int(conf.return_value('max-age')) - - # Determine bridge member interface (currently configured) - for intf in conf.list_nodes('member interface'): - # defaults are stored in util.py (they can't be here as all interface - # scripts use the function) - memberconf = get_bridge_member_config(conf, bridge['intf'], intf) - if memberconf: - memberconf['name'] = intf - bridge['member'].append(memberconf) - - # Determine bridge member interface (currently effective) - to determine which - # interfaces is no longer assigend to the bridge and thus can be removed - eff_intf = conf.list_effective_nodes('member interface') - act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = list_diff(eff_intf, act_intf) - - # Priority for this bridge - if conf.exists('priority'): - bridge['priority'] = int(conf.return_value('priority')) - - # Enable spanning tree protocol - if conf.exists('stp'): - bridge['stp'] = 1 - - # retrieve VRF instance - if conf.exists('vrf'): - bridge['vrf'] = conf.return_value('vrf') + conf = Config() + base = ['interfaces', 'bridge'] + bridge = get_interface_dict(conf, base) + + # determine which members have been removed + tmp = node_changed(conf, ['member', 'interface']) + if tmp: + if 'member' in bridge: + bridge['member'].update({'interface_remove': tmp }) + else: + bridge.update({'member': {'interface_remove': tmp }}) + + if 'member' in bridge and 'interface' in bridge['member']: + # XXX TT2665 we need a copy of the dict keys for iteration, else we will get: + # RuntimeError: dictionary changed size during iteration + for interface in list(bridge['member']['interface']): + for key in ['cost', 'priority']: + if interface == key: + del bridge['member']['interface'][key] + continue + + # the default dictionary is not properly paged into the dict (see T2665) + # thus we will ammend it ourself + default_member_values = defaults(base + ['member', 'interface']) + for interface, interface_config in bridge['member']['interface'].items(): + interface_config.update(default_member_values) + + # Check if we are a member of another bridge device + tmp = is_member(conf, interface, 'bridge') + if tmp and tmp != bridge['ifname']: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp: + interface_config.update({'is_bond_member' : tmp}) + + # Bridge members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) return bridge def verify(bridge): - if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + if 'deleted' in bridge: + return None - vrf_name = bridge['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + verify_dhcpv6(bridge) + verify_vrf(bridge) - conf = Config() - for intf in bridge['member']: - # the interface must exist prior adding it to a bridge - if intf['name'] not in interfaces(): - raise ConfigError(( - f'Cannot add nonexistent interface "{intf["name"]}" ' - f'to bridge "{bridge["intf"]}"')) + if 'member' in bridge: + member = bridge.get('member') + bridge_name = bridge['ifname'] + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bridge "{bridge_name}", ' - if intf['name'] == 'lo': - raise ConfigError('Loopback interface "lo" can not be added to a bridge') + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bridge') - # bridge members aren't allowed to be members of another bridge - for br in conf.list_nodes('interfaces bridge'): - # it makes no sense to verify ourself in this case - if br == bridge['intf']: - continue + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') - tmp = conf.list_nodes(f'interfaces bridge {br} member interface') - if intf['name'] in tmp: - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it is already a member of bridge "{br}"!')) + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') - # bridge members are not allowed to be bond members - tmp = is_member(conf, intf['name'], 'bonding') - if tmp: - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!')) + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') - # bridge members must not have an assigned address - if has_address_configured(conf, intf['name']): - raise ConfigError(( - f'Cannot add interface "{intf["name"]}" to bridge ' - f'"{bridge["intf"]}", it has an address assigned!')) + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') return None @@ -268,120 +122,12 @@ def generate(bridge): return None def apply(bridge): - br = BridgeIf(bridge['intf']) - - if bridge['deleted']: + br = BridgeIf(bridge['ifname']) + if 'deleted' in bridge: # delete interface br.remove() else: - # enable interface - br.set_admin_state('up') - # set ageing time - br.set_ageing_time(bridge['aging']) - # set bridge forward delay - br.set_forward_delay(bridge['forwarding_delay']) - # set hello time - br.set_hello_time(bridge['hello_time']) - # configure ARP filter configuration - br.set_arp_filter(bridge['ip_disable_arp_filter']) - # configure ARP accept - br.set_arp_accept(bridge['ip_enable_arp_accept']) - # configure ARP announce - br.set_arp_announce(bridge['ip_enable_arp_announce']) - # configure ARP ignore - br.set_arp_ignore(bridge['ip_enable_arp_ignore']) - # IPv6 accept RA - br.set_ipv6_accept_ra(bridge['ipv6_accept_ra']) - # IPv6 address autoconfiguration - br.set_ipv6_autoconf(bridge['ipv6_autoconf']) - # IPv6 forwarding - br.set_ipv6_forwarding(bridge['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect']) - # set max message age - br.set_max_age(bridge['max_age']) - # set bridge priority - br.set_priority(bridge['priority']) - # turn stp on/off - br.set_stp(bridge['stp']) - # enable or disable IGMP querier - br.set_multicast_querier(bridge['igmp_querier']) - # update interface description used e.g. within SNMP - br.set_alias(bridge['description']) - - if bridge['dhcp_client_id']: - br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id'] - - if bridge['dhcp_hostname']: - br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname'] - - if bridge['dhcp_vendor_class_id']: - br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - - if bridge['dhcpv6_prm_only']: - br.dhcp.v6.options['dhcpv6_prm_only'] = True - - if bridge['dhcpv6_temporary']: - br.dhcp.v6.options['dhcpv6_temporary'] = True - - if bridge['dhcpv6_pd_length']: - br.dhcp.v6.options['dhcpv6_pd_length'] = br['dhcpv6_pd_length'] - - if bridge['dhcpv6_pd_interfaces']: - br.dhcp.v6.options['dhcpv6_pd_interfaces'] = br['dhcpv6_pd_interfaces'] - - # assign/remove VRF - br.set_vrf(bridge['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - # (adding members to a fresh bridge changes its MAC too) - for addr in bridge['ipv6_eui64_prefix_remove']: - br.del_ipv6_eui64_address(addr) - - # remove interface from bridge - for intf in bridge['member_remove']: - br.del_port(intf) - - # add interfaces to bridge - for member in bridge['member']: - # if we've come here we already verified the interface doesn't - # have addresses configured so just flush any remaining ones - cmd(f'ip addr flush dev "{member["name"]}"') - br.add_port(member['name']) - - # Change interface MAC address - if bridge['mac']: - br.set_mac(bridge['mac']) - - # Add IPv6 EUI-based addresses (must be done after adding the - # 1st bridge member or setting its MAC) - for addr in bridge['ipv6_eui64_prefix']: - br.add_ipv6_eui64_address(addr) - - # up/down interface - if bridge['disable']: - br.set_admin_state('down') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bridge['address_remove']: - br.del_addr(addr) - for addr in bridge['address']: - br.add_addr(addr) - - STPBridgeIf = STP.enable(BridgeIf) - # configure additional bridge member options - for member in bridge['member']: - i = STPBridgeIf(member['name']) - # configure ARP cache timeout - i.set_arp_cache_tmo(member['arp_cache_tmo']) - # ignore link state changes - i.set_link_detect(member['disable_link_detect']) - # set bridge port path cost - i.set_path_cost(member['cost']) - # set bridge port path priority - i.set_path_priority(member['priority']) + br.update(bridge) return None diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 2d62420a6..44fc9cb9e 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -19,41 +19,26 @@ import os from sys import exit from vyos.config import Config +from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.ifconfig import DummyIf -from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() -def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = ['interfaces', 'dummy', ifname] - - dummy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if dummy == {}: - dummy.update({'deleted' : ''}) - - # store interface instance name in dictionary - dummy.update({'ifname': ifname}) - - # check if we are a member of any bridge - bridge = is_member(conf, ifname, 'bridge') - if bridge: - tmp = {'is_bridge_member' : bridge} - dummy.update(tmp) - +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'dummy'] + dummy = get_interface_dict(conf, base) return dummy def verify(dummy): diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 8b895c4d2..a8df64cce 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -17,295 +17,68 @@ import os from sys import exit -from copy import deepcopy -from netifaces import interfaces -from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.validate import is_member from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_address +from vyos.configverify import verify_vrf +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import EthernetIf from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'deleted': False, - 'duplex': 'auto', - 'flow_control': 'on', - 'hw_id': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'is_bond_member': False, - 'intf': '', - 'offload_gro': 'off', - 'offload_gso': 'off', - 'offload_sg': 'off', - 'offload_tso': 'off', - 'offload_ufo': 'off', - 'speed': 'auto', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], - 'vrf': '' -} - - -def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - conf = Config() - - # check if ethernet interface has been removed - cfg_base = ['interfaces', 'ethernet', ifname] - if not conf.exists(cfg_base): - eth = deepcopy(default_config_data) - eth['intf'] = ifname - eth['deleted'] = True - # we can not bail out early as ethernet interface can not be removed - # Kernel will complain with: RTNETLINK answers: Operation not supported. - # Thus we need to remove individual settings - return eth - - # set new configuration level - conf.set_level(cfg_base) - - eth, disabled = intf_to_dict(conf, default_config_data) - - # disable ethernet flow control (pause frames) - if conf.exists('disable-flow-control'): - eth['flow_control'] = 'off' - - # retrieve real hardware address - if conf.exists('hw-id'): - eth['hw_id'] = conf.return_value('hw-id') - - # interface duplex - if conf.exists('duplex'): - eth['duplex'] = conf.return_value('duplex') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - eth['ip_proxy_arp_pvlan'] = 1 - - # check if we are a member of any bond - eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding') - - # GRO (generic receive offload) - if conf.exists('offload-options generic-receive'): - eth['offload_gro'] = conf.return_value('offload-options generic-receive') - - # GSO (generic segmentation offload) - if conf.exists('offload-options generic-segmentation'): - eth['offload_gso'] = conf.return_value('offload-options generic-segmentation') - - # scatter-gather option - if conf.exists('offload-options scatter-gather'): - eth['offload_sg'] = conf.return_value('offload-options scatter-gather') - - # TSO (TCP segmentation offloading) - if conf.exists('offload-options tcp-segmentation'): - eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation') - - # UDP fragmentation offloading - if conf.exists('offload-options udp-fragmentation'): - eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation') - - # interface speed - if conf.exists('speed'): - eth['speed'] = conf.return_value('speed') - - # remove default IPv6 link-local address if member of a bond - if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']: - eth['ipv6_eui64_prefix'].remove('fe80::/64') - eth['ipv6_eui64_prefix_remove'].append('fe80::/64') - - add_to_dict(conf, disabled, eth, 'vif', 'vif') - add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s') - - return eth - +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'ethernet'] + ethernet = get_interface_dict(conf, base) + return ethernet -def verify(eth): - if eth['deleted']: +def verify(ethernet): + if 'deleted' in ethernet: return None - if eth['intf'] not in interfaces(): - raise ConfigError(f"Interface ethernet {eth['intf']} does not exist") + verify_interface_exists(ethernet) - if eth['speed'] == 'auto': - if eth['duplex'] != 'auto': + if ethernet.get('speed', None) == 'auto': + if ethernet.get('duplex', None) != 'auto': raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') - if eth['duplex'] == 'auto': - if eth['speed'] != 'auto': + if ethernet.get('duplex', None) == 'auto': + if ethernet.get('speed', None) != 'auto': raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') - if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member'] + verify_dhcpv6(ethernet) + verify_address(ethernet) + verify_vrf(ethernet) - if ( memberof - and ( eth['address'] - or eth['ipv6_eui64_prefix'] - or eth['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{eth["intf"]}" ' - f'as it is a member of "{memberof}"!')) - - if eth['vrf']: - if eth['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{eth["vrf"]}" does not exist') - - if memberof: - raise ConfigError(( - f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" ' - f'and "{memberof}" at the same time!')) - - if eth['mac'] and eth['is_bond_member']: - print('WARNING: "mac {0}" command will be ignored because {1} is a part of {2}'\ - .format(eth['mac'], eth['intf'], eth['is_bond_member'])) + if {'is_bond_member', 'mac'} <= set(ethernet): + print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' + f'is a member of bond "{is_bond_member}"'.format(**ethernet)) # use common function to verify VLAN configuration - verify_vlan_config(eth) + verify_vlan_config(ethernet) return None -def generate(eth): +def generate(ethernet): return None -def apply(eth): - e = EthernetIf(eth['intf']) - if eth['deleted']: - # apply all vlans to interface (they need removing too) - apply_all_vlans(e, eth) - +def apply(ethernet): + e = EthernetIf(ethernet['ifname']) + if 'deleted' in ethernet: # delete interface e.remove() else: - # update interface description used e.g. within SNMP - e.set_alias(eth['description']) - - if eth['dhcp_client_id']: - e.dhcp.v4.options['client_id'] = eth['dhcp_client_id'] - - if eth['dhcp_hostname']: - e.dhcp.v4.options['hostname'] = eth['dhcp_hostname'] - - if eth['dhcp_vendor_class_id']: - e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id'] - - if eth['dhcpv6_prm_only']: - e.dhcp.v6.options['dhcpv6_prm_only'] = True - - if eth['dhcpv6_temporary']: - e.dhcp.v6.options['dhcpv6_temporary'] = True - - if eth['dhcpv6_pd_length']: - e.dhcp.v6.options['dhcpv6_pd_length'] = eth['dhcpv6_pd_length'] - - if eth['dhcpv6_pd_interfaces']: - e.dhcp.v6.options['dhcpv6_pd_interfaces'] = eth['dhcpv6_pd_interfaces'] - - # ignore link state changes - e.set_link_detect(eth['disable_link_detect']) - # disable ethernet flow control (pause frames) - e.set_flow_control(eth['flow_control']) - # configure ARP cache timeout in milliseconds - e.set_arp_cache_tmo(eth['ip_arp_cache_tmo']) - # configure ARP filter configuration - e.set_arp_filter(eth['ip_disable_arp_filter']) - # configure ARP accept - e.set_arp_accept(eth['ip_enable_arp_accept']) - # configure ARP announce - e.set_arp_announce(eth['ip_enable_arp_announce']) - # configure ARP ignore - e.set_arp_ignore(eth['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - e.set_proxy_arp(eth['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan']) - # IPv6 accept RA - e.set_ipv6_accept_ra(eth['ipv6_accept_ra']) - # IPv6 address autoconfiguration - e.set_ipv6_autoconf(eth['ipv6_autoconf']) - # IPv6 forwarding - e.set_ipv6_forwarding(eth['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in eth['ipv6_eui64_prefix_remove']: - e.del_ipv6_eui64_address(addr) - - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed. Skip if bond member. - if not eth['is_bond_member']: - if eth['mac']: - e.set_mac(eth['mac']) - elif eth['hw_id']: - e.set_mac(eth['hw_id']) - - # Add IPv6 EUI-based addresses - for addr in eth['ipv6_eui64_prefix']: - e.add_ipv6_eui64_address(addr) - - # Maximum Transmission Unit (MTU) - e.set_mtu(eth['mtu']) - - # GRO (generic receive offload) - e.set_gro(eth['offload_gro']) - - # GSO (generic segmentation offload) - e.set_gso(eth['offload_gso']) - - # scatter-gather option - e.set_sg(eth['offload_sg']) - - # TSO (TCP segmentation offloading) - e.set_tso(eth['offload_tso']) - - # UDP fragmentation offloading - e.set_ufo(eth['offload_ufo']) - - # Set physical interface speed and duplex - e.set_speed_duplex(eth['speed'], eth['duplex']) - - # Enable/Disable interface - if eth['disable']: - e.set_admin_state('down') - else: - e.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in eth['address_remove']: - e.del_addr(addr) - for addr in eth['address']: - e.add_addr(addr) - - # assign/remove VRF (ONLY when not a member of a bridge or bond, - # otherwise 'nomaster' removes it from it) - if not ( eth['is_bridge_member'] or eth['is_bond_member'] ): - e.set_vrf(eth['vrf']) - - # re-add ourselves to any bridge we might have fallen out of - if eth['is_bridge_member']: - e.add_to_bridge(eth['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(e, eth) + e.update(ethernet) if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 31f6eb6b5..cc2cf025a 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -21,102 +21,40 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete from vyos.ifconfig import GeneveIf -from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp': 0, - 'is_bridge_member': False, - 'mtu': 1500, - 'remote': '', - 'vni': '' -} - -def get_config(): - geneve = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces geneve ' + geneve['intf']): - geneve['deleted'] = True - return geneve - - # set new configuration level - conf.set_level('interfaces geneve ' + geneve['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - geneve['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - geneve['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - geneve['disable'] = True - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - geneve['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - geneve['ip_proxy_arp'] = 1 - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - geneve['mtu'] = int(conf.return_value('mtu')) - - # Remote address of GENEVE tunnel - if conf.exists('remote'): - geneve['remote'] = conf.return_value('remote') - - # Virtual Network Identifier - if conf.exists('vni'): - geneve['vni'] = conf.return_value('vni') - +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'geneve'] + geneve = get_interface_dict(conf, base) return geneve - def verify(geneve): - if geneve['deleted']: - if geneve['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{geneve["intf"]}" as it is a ' - f'member of bridge "{geneve["is_bridge_member"]}"!')) - + if 'deleted' in geneve: + verify_bridge_delete(geneve) return None - if geneve['is_bridge_member'] and geneve['address']: - raise ConfigError(( - f'Cannot assign address to interface "{geneve["intf"]}" ' - f'as it is a member of bridge "{geneve["is_bridge_member"]}"!')) + verify_address(geneve) - if not geneve['remote']: - raise ConfigError('GENEVE remote must be configured') + if 'remote' not in geneve: + raise ConfigError('Remote side must be configured') - if not geneve['vni']: - raise ConfigError('GENEVE VNI must be configured') + if 'vni' not in geneve: + raise ConfigError('VNI must be configured') return None @@ -127,13 +65,13 @@ def generate(geneve): def apply(geneve): # Check if GENEVE interface already exists - if geneve['intf'] in interfaces(): - g = GeneveIf(geneve['intf']) + if geneve['ifname'] in interfaces(): + g = GeneveIf(geneve['ifname']) # GENEVE is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. g.remove() - if not geneve['deleted']: + if 'deleted' not in geneve: # GENEVE interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig @@ -144,32 +82,8 @@ def apply(geneve): conf['remote'] = geneve['remote'] # Finally create the new interface - g = GeneveIf(geneve['intf'], **conf) - # update interface description used e.g. by SNMP - g.set_alias(geneve['description']) - # Maximum Transfer Unit (MTU) - g.set_mtu(geneve['mtu']) - - # configure ARP cache timeout in milliseconds - g.set_arp_cache_tmo(geneve['ip_arp_cache_tmo']) - # Enable proxy-arp on this interface - g.set_proxy_arp(geneve['ip_proxy_arp']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in geneve['address']: - g.add_addr(addr) - - # As the GENEVE interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not geneve['disable']: - g.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if geneve['is_bridge_member']: - g.add_to_bridge(geneve['is_bridge_member']) + g = GeneveIf(geneve['ifname'], **conf) + g.update(geneve) return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 4ff0bcb57..8250a3df8 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -21,200 +21,68 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.ifconfig import L2TPv3If, Interface +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import L2TPv3If +from vyos.util import check_kmod +from vyos.validate import is_addr_assigned from vyos import ConfigError -from vyos.util import call -from vyos.validate import is_member, is_addr_assigned - from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'encapsulation': 'udp', - 'local_address': '', - 'local_port': 5000, - 'intf': '', - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'mtu': 1488, - 'peer_session_id': '', - 'peer_tunnel_id': '', - 'remote_address': '', - 'remote_port': 5000, - 'session_id': '', - 'tunnel_id': '' -} - -def check_kmod(): - modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - -def get_config(): - l2tpv3 = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member of a bridge - l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']): - l2tpv3['deleted'] = True - interface = l2tpv3['intf'] - - # to delete the l2tpv3 interface we need the current tunnel_id and session_id - if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'): - l2tpv3['tunnel_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} tunnel-id') - - if conf.exists_effective(f'interfaces l2tpv3 {interface} session-id'): - l2tpv3['session_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} session-id') - - return l2tpv3 - - # set new configuration level - conf.set_level('interfaces l2tpv3 ' + l2tpv3['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - l2tpv3['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - l2tpv3['description'] = conf.return_value('description') - - # get tunnel destination port - if conf.exists('destination-port'): - l2tpv3['remote_port'] = int(conf.return_value('destination-port')) - - # Disable this interface - if conf.exists('disable'): - l2tpv3['disable'] = True - - # get tunnel encapsulation type - if conf.exists('encapsulation'): - l2tpv3['encapsulation'] = conf.return_value('encapsulation') - - # get tunnel local ip address - if conf.exists('local-ip'): - l2tpv3['local_address'] = conf.return_value('local-ip') - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - l2tpv3['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') +k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] - # Remove the default link-local address if set. - if not ( conf.exists('ipv6 address no-default-link-local') or - l2tpv3['is_bridge_member'] ): - # add the link-local by default to make IPv6 work - l2tpv3['ipv6_eui64_prefix'].append('fe80::/64') - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - l2tpv3['ipv6_forwarding'] = 0 +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'l2tpv3'] + l2tpv3 = get_interface_dict(conf, base) - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # L2TPv3 is "special" the default MTU is 1488 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + l2tpv3['mtu'] = '1488' - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']: - l2tpv3['ipv6_accept_ra'] = 2 + # To delete an l2tpv3 interface we need the current tunnel and session-id + if 'deleted' in l2tpv3: + tmp = leaf_node_changed(conf, ['tunnel-id']) + l2tpv3.update({'tunnel_id': tmp}) - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - l2tpv3['mtu'] = int(conf.return_value('mtu')) - - # Remote session id - if conf.exists('peer-session-id'): - l2tpv3['peer_session_id'] = conf.return_value('peer-session-id') - - # Remote tunnel id - if conf.exists('peer-tunnel-id'): - l2tpv3['peer_tunnel_id'] = conf.return_value('peer-tunnel-id') - - # Remote address of L2TPv3 tunnel - if conf.exists('remote-ip'): - l2tpv3['remote_address'] = conf.return_value('remote-ip') - - # Local session id - if conf.exists('session-id'): - l2tpv3['session_id'] = conf.return_value('session-id') - - # get local tunnel port - if conf.exists('source-port'): - l2tpv3['local_port'] = conf.return_value('source-port') - - # get local tunnel id - if conf.exists('tunnel-id'): - l2tpv3['tunnel_id'] = conf.return_value('tunnel-id') + tmp = leaf_node_changed(conf, ['session-id']) + l2tpv3.update({'session_id': tmp}) return l2tpv3 - def verify(l2tpv3): - interface = l2tpv3['intf'] - - if l2tpv3['deleted']: - if l2tpv3['is_bridge_member']: - raise ConfigError(( - f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a ' - f'member of bridge "{l2tpv3["is_bridge_member"]}"!')) - + if 'deleted' in l2tpv3: + verify_bridge_delete(l2tpv3) return None - if not l2tpv3['local_address']: - raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}') - - if not is_addr_assigned(l2tpv3['local_address']): - raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}') + interface = l2tpv3['ifname'] - if not l2tpv3['remote_address']: - raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}') + for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id', + 'session_id', 'peer_session_id']: + if key not in l2tpv3: + tmp = key.replace('_', '-') + raise ConfigError(f'L2TPv3 {tmp} must be configured!') - if not l2tpv3['tunnel_id']: - raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}') - - if not l2tpv3['peer_tunnel_id']: - raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}') - - if not l2tpv3['session_id']: - raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}') - - if not l2tpv3['peer_session_id']: - raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}') - - if ( l2tpv3['is_bridge_member'] - and ( l2tpv3['address'] - or l2tpv3['ipv6_eui64_prefix'] - or l2tpv3['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{l2tpv3["intf"]}" ' - f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!')) + if not is_addr_assigned(l2tpv3['local_ip']): + raise ConfigError('L2TPv3 local-ip address ' + '"{local_ip}" is not configured!'.format(**l2tpv3)) + verify_address(l2tpv3) return None - def generate(l2tpv3): return None @@ -225,65 +93,34 @@ def apply(l2tpv3): conf = deepcopy(L2TPv3If.get_config()) # Check if L2TPv3 interface already exists - if l2tpv3['intf'] in interfaces(): + if l2tpv3['ifname'] in interfaces(): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply # always delete it first. conf['session_id'] = l2tpv3['session_id'] conf['tunnel_id'] = l2tpv3['tunnel_id'] - l = L2TPv3If(l2tpv3['intf'], **conf) + l = L2TPv3If(l2tpv3['ifname'], **conf) l.remove() - if not l2tpv3['deleted']: + if 'deleted' not in l2tpv3: conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id'] - conf['local_port'] = l2tpv3['local_port'] - conf['remote_port'] = l2tpv3['remote_port'] + conf['local_port'] = l2tpv3['source_port'] + conf['remote_port'] = l2tpv3['destination_port'] conf['encapsulation'] = l2tpv3['encapsulation'] - conf['local_address'] = l2tpv3['local_address'] - conf['remote_address'] = l2tpv3['remote_address'] + conf['local_address'] = l2tpv3['local_ip'] + conf['remote_address'] = l2tpv3['remote_ip'] conf['session_id'] = l2tpv3['session_id'] conf['tunnel_id'] = l2tpv3['tunnel_id'] conf['peer_session_id'] = l2tpv3['peer_session_id'] # Finally create the new interface - l = L2TPv3If(l2tpv3['intf'], **conf) - # update interface description used e.g. by SNMP - l.set_alias(l2tpv3['description']) - # Maximum Transfer Unit (MTU) - l.set_mtu(l2tpv3['mtu']) - # IPv6 accept RA - l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra']) - # IPv6 address autoconfiguration - l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf']) - # IPv6 forwarding - l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in l2tpv3['address']: - l.add_addr(addr) - - # IPv6 EUI-based addresses - for addr in l2tpv3['ipv6_eui64_prefix']: - l.add_ipv6_eui64_address(addr) - - # As the interface is always disabled first when changing parameters - # we will only re-enable the interface if it is not administratively - # disabled - if not l2tpv3['disable']: - l.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if l2tpv3['is_bridge_member']: - l.add_to_bridge(l2tpv3['is_bridge_member']) + l = L2TPv3If(l2tpv3['ifname'], **conf) + l.update(l2tpv3) return None if __name__ == '__main__': try: - check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 2368f88a9..30a27abb4 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -18,31 +18,24 @@ import os from sys import exit -from vyos.ifconfig import LoopbackIf from vyos.config import Config -from vyos import ConfigError, airbag +from vyos.configdict import get_interface_dict +from vyos.ifconfig import LoopbackIf +from vyos import ConfigError +from vyos import airbag airbag.enable() -def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = ['interfaces', 'loopback', ifname] - - loopback = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if loopback == {}: - loopback.update({'deleted' : ''}) - - # store interface instance name in dictionary - loopback.update({'ifname': ifname}) - +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'loopback'] + loopback = get_interface_dict(conf, base) return loopback def verify(loopback): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 56273f71a..2866ccc0a 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -20,16 +20,14 @@ from copy import deepcopy from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.ifconfig import MACsecIf from vyos.template import render from vyos.util import call -from vyos.validate import is_member from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_source_interface -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -37,51 +35,29 @@ airbag.enable() # XXX: wpa_supplicant works on the source interface wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' -def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() base = ['interfaces', 'macsec'] - default_values = defaults(base) - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] + macsec = get_interface_dict(conf, base) - macsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # Check if interface has been removed - if macsec == {}: - tmp = { - 'deleted' : '', - 'source_interface' : conf.return_effective_value( + if 'deleted' in macsec: + source_interface = conf.return_effective_value( base + ['source-interface']) - } - macsec.update(tmp) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - macsec = dict_merge(default_values, macsec) - - # Add interface instance name into dictionary - macsec.update({'ifname': ifname}) - - # Check if we are a member of any bridge - bridge = is_member(conf, ifname, 'bridge') - if bridge: - tmp = {'is_bridge_member' : bridge} - macsec.update(tmp) + macsec.update({'source_interface': source_interface}) return macsec def verify(macsec): - if 'deleted' in macsec.keys(): + if 'deleted' in macsec: verify_bridge_delete(macsec) return None @@ -89,18 +65,18 @@ def verify(macsec): verify_vrf(macsec) verify_address(macsec) - if not (('security' in macsec.keys()) and - ('cipher' in macsec['security'].keys())): + if not (('security' in macsec) and + ('cipher' in macsec['security'])): raise ConfigError( 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) - if (('security' in macsec.keys()) and - ('encrypt' in macsec['security'].keys())): + if (('security' in macsec) and + ('encrypt' in macsec['security'])): tmp = macsec.get('security') - if not (('mka' in tmp.keys()) and - ('cak' in tmp['mka'].keys()) and - ('ckn' in tmp['mka'].keys())): + if not (('mka' in tmp) and + ('cak' in tmp['mka']) and + ('ckn' in tmp['mka'])): raise ConfigError('Missing mandatory MACsec security ' 'keys as encryption is enabled!') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 1420b4116..958b305dd 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -192,9 +192,12 @@ def getDefaultServer(network, topology, devtype): return server -def get_config(): +def get_config(config=None): openvpn = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 3ee57e83c..1b4b9e4ee 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -15,58 +15,43 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jmespath from sys import exit from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.configverify import verify_source_interface from vyos.configverify import verify_vrf from vyos.template import render from vyos.util import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() -def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() base = ['interfaces', 'pppoe'] - default_values = defaults(base) - # PPPoE is "special" the default MTU is 1492 - update accordingly - default_values['mtu'] = '1492' - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] + pppoe = get_interface_dict(conf, base) - pppoe = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if pppoe == {}: - pppoe.update({'deleted' : ''}) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - pppoe = dict_merge(default_values, pppoe) - - # Add interface instance name into dictionary - pppoe.update({'ifname': ifname}) + # PPPoE is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + pppoe['mtu'] = '1492' return pppoe def verify(pppoe): - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # bail out early return None @@ -92,7 +77,7 @@ def generate(pppoe): config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # stop DHCPv6-PD client call(f'systemctl stop dhcp6c@{ifname}.service') # Hang-up PPPoE connection @@ -121,20 +106,19 @@ def generate(pppoe): render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', pppoe, trim_blocks=True, permission=0o755) - tmp = jmespath.search('dhcpv6_options.prefix_delegation.interface', pppoe) - if tmp and len(tmp) > 0: + if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']: # ipv6.tmpl relies on ifname - this should be made consitent in the # future better then double key-ing the same value - render(config_wide_dhcp6c, 'dhcp-client/ipv6_new.tmpl', pppoe, trim_blocks=True) + render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True) return None def apply(pppoe): - if 'deleted' in pppoe.keys(): + if 'deleted' in pppoe: # bail out early return None - if 'disable' not in pppoe.keys(): + if 'disable' not in pppoe: # Dial PPPoE connection call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 70710e97c..59edca1cc 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -18,112 +18,69 @@ import os from copy import deepcopy from sys import exit -from netifaces import interfaces from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import MACVLANIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import MACVLANIf +from vyos.validate import is_member from vyos import ConfigError from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'deleted': False, - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp_pvlan': 0, - 'source_interface': '', - 'source_interface_changed': False, - 'mode': 'private', - 'vif_s': {}, - 'vif_s_remove': [], - 'vif': {}, - 'vif_remove': [], - 'vrf': '' -} - -def get_config(): - peth = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - peth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # Check if interface has been removed - cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']] - if not conf.exists(cfg_base): - peth['deleted'] = True - return peth - - # set new configuration level - conf.set_level(cfg_base) - - peth, disabled = intf_to_dict(conf, default_config_data) - - # ARP cache entry timeout in seconds - if conf.exists(['ip', 'arp-cache-timeout']): - peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout'])) - - # Enable private VLAN proxy ARP on this interface - if conf.exists(['ip', 'proxy-arp-pvlan']): - peth['ip_proxy_arp_pvlan'] = 1 +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at + least the interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'pseudo-ethernet'] + peth = get_interface_dict(conf, base) - # Physical interface - if conf.exists(['source-interface']): - peth['source_interface'] = conf.return_value(['source-interface']) - tmp = conf.return_effective_value(['source-interface']) - if tmp != peth['source_interface']: - peth['source_interface_changed'] = True + mode = leaf_node_changed(conf, ['mode']) + if mode: + peth.update({'mode_old' : mode}) - # MACvlan mode - if conf.exists(['mode']): - peth['mode'] = conf.return_value(['mode']) + # Check if source-interface is member of a bridge device + if 'source_interface' in peth: + bridge = is_member(conf, peth['source_interface'], 'bridge') + if bridge: + peth.update({'source_interface_is_bridge_member' : bridge}) - add_to_dict(conf, disabled, peth, 'vif', 'vif') - add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s') + # Check if we are a member of a bond device + bond = is_member(conf, peth['source_interface'], 'bonding') + if bond: + peth.update({'source_interface_is_bond_member' : bond}) return peth def verify(peth): - if peth['deleted']: - if peth['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{peth["intf"]}" as it is a ' - f'member of bridge "{peth["is_bridge_member"]}"!')) - + if 'deleted' in peth: + verify_bridge_delete(peth) return None - if not peth['source_interface']: - raise ConfigError(( - f'Link device must be set for pseudo-ethernet "{peth["intf"]}"')) - - if not peth['source_interface'] in interfaces(): - raise ConfigError(( - f'Pseudo-ethernet "{peth["intf"]}" link device does not exist')) - - if ( peth['is_bridge_member'] - and ( peth['address'] - or peth['ipv6_eui64_prefix'] - or peth['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{peth["intf"]}" ' - f'as it is a member of bridge "{peth["is_bridge_member"]}"!')) + verify_source_interface(peth) + verify_vrf(peth) + verify_address(peth) - if peth['vrf']: - if peth['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{peth["vrf"]}" does not exist') + if 'source_interface_is_bridge_member' in peth: + raise ConfigError( + 'Source interface "{source_interface}" can not be used as it is already a ' + 'member of bridge "{source_interface_is_bridge_member}"!'.format(**peth)) - if peth['is_bridge_member']: - raise ConfigError(( - f'Interface "{peth["intf"]}" cannot be member of VRF ' - f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} ' - f'at the same time!')) + if 'source_interface_is_bond_member' in peth: + raise ConfigError( + 'Source interface "{source_interface}" can not be used as it is already a ' + 'member of bond "{source_interface_is_bond_member}"!'.format(**peth)) # use common function to verify VLAN configuration verify_vlan_config(peth) @@ -133,17 +90,16 @@ def generate(peth): return None def apply(peth): - if peth['deleted']: + if 'deleted' in peth: # delete interface - MACVLANIf(peth['intf']).remove() + MACVLANIf(peth['ifname']).remove() return None # Check if MACVLAN interface already exists. Parameters like the underlaying - # source-interface device can not be changed on the fly and the interface - # needs to be recreated from the bottom. - if peth['intf'] in interfaces(): - if peth['source_interface_changed']: - MACVLANIf(peth['intf']).remove() + # source-interface device or mode can not be changed on the fly and the + # interface needs to be recreated from the bottom. + if 'mode_old' in peth: + MACVLANIf(peth['ifname']).remove() # MACVLAN interface needs to be created on-block instead of passing a ton # of arguments, I just use a dict that is managed by vyos.ifconfig @@ -155,98 +111,8 @@ def apply(peth): # It is safe to "re-create" the interface always, there is a sanity check # that the interface will only be create if its non existent - p = MACVLANIf(peth['intf'], **conf) - - # update interface description used e.g. within SNMP - p.set_alias(peth['description']) - - if peth['dhcp_client_id']: - p.dhcp.v4.options['client_id'] = peth['dhcp_client_id'] - - if peth['dhcp_hostname']: - p.dhcp.v4.options['hostname'] = peth['dhcp_hostname'] - - if peth['dhcp_vendor_class_id']: - p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id'] - - if peth['dhcpv6_prm_only']: - p.dhcp.v6.options['dhcpv6_prm_only'] = True - - if peth['dhcpv6_temporary']: - p.dhcp.v6.options['dhcpv6_temporary'] = True - - if peth['dhcpv6_pd_length']: - p.dhcp.v6.options['dhcpv6_pd_length'] = peth['dhcpv6_pd_length'] - - if peth['dhcpv6_pd_interfaces']: - p.dhcp.v6.options['dhcpv6_pd_interfaces'] = peth['dhcpv6_pd_interfaces'] - - # ignore link state changes - p.set_link_detect(peth['disable_link_detect']) - # configure ARP cache timeout in milliseconds - p.set_arp_cache_tmo(peth['ip_arp_cache_tmo']) - # configure ARP filter configuration - p.set_arp_filter(peth['ip_disable_arp_filter']) - # configure ARP accept - p.set_arp_accept(peth['ip_enable_arp_accept']) - # configure ARP announce - p.set_arp_announce(peth['ip_enable_arp_announce']) - # configure ARP ignore - p.set_arp_ignore(peth['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - p.set_proxy_arp(peth['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan']) - # IPv6 accept RA - p.set_ipv6_accept_ra(peth['ipv6_accept_ra']) - # IPv6 address autoconfiguration - p.set_ipv6_autoconf(peth['ipv6_autoconf']) - # IPv6 forwarding - p.set_ipv6_forwarding(peth['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not peth['is_bridge_member']: - p.set_vrf(peth['vrf']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in peth['ipv6_eui64_prefix_remove']: - p.del_ipv6_eui64_address(addr) - - # Change interface MAC address - if peth['mac']: - p.set_mac(peth['mac']) - - # Add IPv6 EUI-based addresses - for addr in peth['ipv6_eui64_prefix']: - p.add_ipv6_eui64_address(addr) - - # Change interface mode - p.set_mode(peth['mode']) - - # Enable/Disable interface - if peth['disable']: - p.set_admin_state('down') - else: - p.set_admin_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in peth['address_remove']: - p.del_addr(addr) - for addr in peth['address']: - p.add_addr(addr) - - # re-add ourselves to any bridge we might have fallen out of - if peth['is_bridge_member']: - p.add_to_bridge(peth['is_bridge_member']) - - # apply all vlans to interface - apply_all_vlans(p, peth) - + p = MACVLANIf(peth['ifname'], **conf) + p.update(peth) return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index ea15a7fb7..11d8d6edc 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -397,12 +397,16 @@ def ip_proto (afi): return 6 if afi == IP6 else 4 -def get_config(): +def get_config(config=None): ifname = os.environ.get('VYOS_TAGNODE_VALUE','') if not ifname: raise ConfigError('Interface not specified') - config = Config() + if config: + config = config + else: + config = Config() + conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data) options = conf.options changes = conf.changes diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 39db814b4..bea3aa25b 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -21,197 +21,64 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface from vyos.ifconfig import VXLANIf, Interface -from vyos.validate import is_member from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'group': '', - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': [], - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'is_bridge_member': False, - 'source_address': '', - 'source_interface': '', - 'mtu': 1450, - 'remote': '', - 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - 'vni': '' -} - -def get_config(): - vxlan = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists('interfaces vxlan ' + vxlan['intf']): - vxlan['deleted'] = True - return vxlan - - # set new configuration level - conf.set_level('interfaces vxlan ' + vxlan['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - vxlan['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - vxlan['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - vxlan['disable'] = True - - # VXLAN multicast grou - if conf.exists('group'): - vxlan['group'] = conf.return_value('group') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - vxlan['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - vxlan['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - vxlan['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - vxlan['ip_enable_arp_ignore'] = 1 - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - vxlan['ip_proxy_arp'] = 1 - - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - vxlan['ipv6_autoconf'] = 1 - - # Get prefixes for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - - # Remove the default link-local address if set. - if not ( conf.exists('ipv6 address no-default-link-local') - or vxlan['is_bridge_member'] ): - # add the link-local by default to make IPv6 work - vxlan['ipv6_eui64_prefix'].append('fe80::/64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - vxlan['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']: - vxlan['ipv6_accept_ra'] = 2 - - # VXLAN source address - if conf.exists('source-address'): - vxlan['source_address'] = conf.return_value('source-address') - - # VXLAN underlay interface - if conf.exists('source-interface'): - vxlan['source_interface'] = conf.return_value('source-interface') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vxlan['mtu'] = int(conf.return_value('mtu')) - - # Remote address of VXLAN tunnel - if conf.exists('remote'): - vxlan['remote'] = conf.return_value('remote') - - # Remote port of VXLAN tunnel - if conf.exists('port'): - vxlan['remote_port'] = int(conf.return_value('port')) - - # Virtual Network Identifier - if conf.exists('vni'): - vxlan['vni'] = conf.return_value('vni') +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'vxlan'] + vxlan = get_interface_dict(conf, base) + + # VXLAN is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + vxlan['mtu'] = '1450' return vxlan - def verify(vxlan): - if vxlan['deleted']: - if vxlan['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{vxlan["intf"]}" as it is a ' - f'member of bridge "{vxlan["is_bridge_member"]}"!')) - + if 'deleted' in vxlan: + verify_bridge_delete(vxlan) return None - if vxlan['mtu'] < 1500: + if int(vxlan['mtu']) < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - if vxlan['group']: - if not vxlan['source_interface']: + if 'group' in vxlan: + if 'source_interface' not in vxlan: raise ConfigError('Multicast VXLAN requires an underlaying interface ') - if not vxlan['source_interface'] in interfaces(): - raise ConfigError('VXLAN source interface does not exist') + verify_source_interface(vxlan) - if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): + if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): raise ConfigError('Group, remote or source-address must be configured') - if not vxlan['vni']: + if 'vni' not in vxlan: raise ConfigError('Must configure VNI for VXLAN') - if vxlan['source_interface']: + if 'source_interface' in vxlan: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) - if underlay_mtu < (vxlan['mtu'] + 50): + if underlay_mtu < (int(vxlan['mtu']) + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ - 'MTU is to small ({})'.format(underlay_mtu)) - - if ( vxlan['is_bridge_member'] - and ( vxlan['address'] - or vxlan['ipv6_eui64_prefix'] - or vxlan['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{vxlan["intf"]}" ' - f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!')) + f'MTU is to small ({underlay_mtu} bytes)') + verify_address(vxlan) return None @@ -221,73 +88,26 @@ def generate(vxlan): def apply(vxlan): # Check if the VXLAN interface already exists - if vxlan['intf'] in interfaces(): - v = VXLANIf(vxlan['intf']) + if vxlan['ifname'] in interfaces(): + v = VXLANIf(vxlan['ifname']) # VXLAN is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() - if not vxlan['deleted']: + if 'deleted' not in vxlan: # VXLAN interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig conf = deepcopy(VXLANIf.get_config()) # Assign VXLAN instance configuration parameters to config dict - conf['vni'] = vxlan['vni'] - conf['group'] = vxlan['group'] - conf['src_address'] = vxlan['source_address'] - conf['src_interface'] = vxlan['source_interface'] - conf['remote'] = vxlan['remote'] - conf['port'] = vxlan['remote_port'] + for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: + if tmp in vxlan: + conf[tmp] = vxlan[tmp] # Finally create the new interface - v = VXLANIf(vxlan['intf'], **conf) - # update interface description used e.g. by SNMP - v.set_alias(vxlan['description']) - # Maximum Transfer Unit (MTU) - v.set_mtu(vxlan['mtu']) - - # configure ARP cache timeout in milliseconds - v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) - # configure ARP filter configuration - v.set_arp_filter(vxlan['ip_disable_arp_filter']) - # configure ARP accept - v.set_arp_accept(vxlan['ip_enable_arp_accept']) - # configure ARP announce - v.set_arp_announce(vxlan['ip_enable_arp_announce']) - # configure ARP ignore - v.set_arp_ignore(vxlan['ip_enable_arp_ignore']) - # Enable proxy-arp on this interface - v.set_proxy_arp(vxlan['ip_proxy_arp']) - # IPv6 accept RA - v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra']) - # IPv6 address autoconfiguration - v.set_ipv6_autoconf(vxlan['ipv6_autoconf']) - # IPv6 forwarding - v.set_ipv6_forwarding(vxlan['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in vxlan['address']: - v.add_addr(addr) - - # IPv6 EUI-based addresses - for addr in vxlan['ipv6_eui64_prefix']: - v.add_ipv6_eui64_address(addr) - - # As the VXLAN interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not vxlan['disable']: - v.set_admin_state('up') - - # re-add ourselves to any bridge we might have fallen out of - if vxlan['is_bridge_member']: - v.add_to_bridge(vxlan['is_bridge_member']) + v = VXLANIf(vxlan['ifname'], **conf) + v.update(vxlan) return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index c24c9a7ce..e7c22da1a 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -15,308 +15,101 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from sys import exit from copy import deepcopy -from netifaces import interfaces from vyos.config import Config -from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete from vyos.ifconfig import WireGuardIf -from vyos.util import chown, chmod_750, call -from vyos.validate import is_member, is_ipv6 +from vyos.util import check_kmod from vyos import ConfigError - from vyos import airbag airbag.enable() -kdir = r'/config/auth/wireguard' - -default_config_data = { - 'intfc': '', - 'address': [], - 'address_remove': [], - 'description': '', - 'listen_port': '', - 'deleted': False, - 'disable': False, - 'fwmark': 0, - 'is_bridge_member': False, - 'mtu': 1420, - 'peer': [], - 'peer_remove': [], # stores public keys of peers to remove - 'pk': f'{kdir}/default/private.key', - 'vrf': '' -} - -def _check_kmod(): - modules = ['wireguard'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - - -def _migrate_default_keys(): - if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): - location = f'{kdir}/default' - if not os.path.exists(location): - os.makedirs(location) - - chown(location, 'root', 'vyattacfg') - chmod_750(location) - os.rename(f'{kdir}/private.key', f'{location}/private.key') - os.rename(f'{kdir}/public.key', f'{location}/public.key') - - -def get_config(): - conf = Config() +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() base = ['interfaces', 'wireguard'] - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - wg = deepcopy(default_config_data) - wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - - # check if interface is member if a bridge - wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge') - - # Check if interface has been removed - if not conf.exists(base + [wg['intf']]): - wg['deleted'] = True - return wg - - conf.set_level(base + [wg['intf']]) - - # retrieve configured interface addresses - if conf.exists(['address']): - wg['address'] = conf.return_values(['address']) - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values(['address']) - wg['address_remove'] = list_diff(eff_addr, wg['address']) - - # retrieve interface description - if conf.exists(['description']): - wg['description'] = conf.return_value(['description']) - - # disable interface - if conf.exists(['disable']): - wg['disable'] = True - - # local port to listen on - if conf.exists(['port']): - wg['listen_port'] = conf.return_value(['port']) - - # fwmark value - if conf.exists(['fwmark']): - wg['fwmark'] = int(conf.return_value(['fwmark'])) - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - wg['mtu'] = int(conf.return_value(['mtu'])) - - # retrieve VRF instance - if conf.exists('vrf'): - wg['vrf'] = conf.return_value('vrf') - - # private key - if conf.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, conf.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = conf.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) - for peer in peer_rem: - wg['peer_remove'].append( - conf.return_effective_value(['peer', peer, 'pubkey'])) - - # peer settings - if conf.exists(['peer']): - for p in conf.list_nodes(['peer']): - # set new config level for this peer - conf.set_level(base + [wg['intf'], 'peer', p]) - peer = { - 'allowed-ips': [], - 'address': '', - 'name': p, - 'persistent_keepalive': '', - 'port': '', - 'psk': '', - 'pubkey': '' - } - - # peer allowed-ips - if conf.exists(['allowed-ips']): - peer['allowed-ips'] = conf.return_values(['allowed-ips']) - - # peer address - if conf.exists(['address']): - peer['address'] = conf.return_value(['address']) - - # peer port - if conf.exists(['port']): - peer['port'] = conf.return_value(['port']) - - # persistent-keepalive - if conf.exists(['persistent-keepalive']): - peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) - - # preshared-key - if conf.exists(['preshared-key']): - peer['psk'] = conf.return_value(['preshared-key']) - - # peer pubkeys - if conf.exists(['pubkey']): - key_eff = conf.return_effective_value(['pubkey']) - key_cfg = conf.return_value(['pubkey']) - peer['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - if conf.exists(['disable']): - wg['peer_remove'].append(peer['pubkey']) - else: - wg['peer'].append(peer) - - return wg - - -def verify(wg): - if wg['deleted']: - if wg['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{wg["intf"]}" as it is a member ' - f'of bridge "{wg["is_bridge_member"]}"!')) - + wireguard = get_interface_dict(conf, base) + + # Wireguard is "special" the default MTU is 1420 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + wireguard['mtu'] = '1420' + + # Mangle private key - it has a default so its always valid + wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard) + + # Determine which Wireguard peer has been removed. + # Peers can only be removed with their public key! + tmp = node_changed(conf, ['peer']) + if tmp: + dict = {} + for peer in tmp: + peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey']) + dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict) + wireguard.update(dict) + + return wireguard + +def verify(wireguard): + if 'deleted' in wireguard: + verify_bridge_delete(wireguard) return None - if wg['is_bridge_member'] and wg['address']: - raise ConfigError(( - f'Cannot assign address to interface "{wg["intf"]}" ' - f'as it is a member of bridge "{wg["is_bridge_member"]}"!')) - - if wg['vrf']: - if wg['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{wg["vrf"]}" does not exist') + verify_address(wireguard) + verify_vrf(wireguard) - if wg['is_bridge_member']: - raise ConfigError(( - f'Interface "{wg["intf"]}" cannot be member of VRF ' - f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} ' - f'at the same time!')) + if not os.path.exists(wireguard['private_key']): + raise ConfigError('Wireguard private-key not found! Execute: ' \ + '"run generate wireguard [default-keypair|named-keypairs]"') - if not os.path.exists(wg['pk']): - raise ConfigError('No keys found, generate them by executing:\n' \ - '"run generate wireguard [keypair|named-keypairs]"') + if 'address' not in wireguard: + raise ConfigError('IP address required!') - if not wg['address']: - raise ConfigError(f'IP address required for interface "{wg["intf"]}"!') - - if not wg['peer']: - raise ConfigError(f'Peer required for interface "{wg["intf"]}"!') + if 'peer' not in wireguard: + raise ConfigError('At least one Wireguard peer is required!') # run checks on individual configured WireGuard peer - for peer in wg['peer']: - if not peer['allowed-ips']: - raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!') - - if not peer['pubkey']: - raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!') - - if peer['address'] and not peer['port']: - raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!') + for tmp in wireguard['peer']: + peer = wireguard['peer'][tmp] - if not peer['address'] and peer['port']: - raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!') + if 'allowed_ips' not in peer: + raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') + if 'pubkey' not in peer: + raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') -def apply(wg): - # init wg class - w = WireGuardIf(wg['intf']) + if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): + raise ConfigError('Both Wireguard port and address must be defined ' + f'for peer "{tmp}" if either one of them is set!') - # single interface removal - if wg['deleted']: - w.remove() +def apply(wireguard): + if 'deleted' in wireguard: + WireGuardIf(wireguard['ifname']).remove() return None - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in wg['address_remove']: - w.del_addr(addr) - for addr in wg['address']: - w.add_addr(addr) - - # Maximum Transmission Unit (MTU) - w.set_mtu(wg['mtu']) - - # update interface description used e.g. within SNMP - w.set_alias(wg['description']) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not wg['is_bridge_member']: - w.set_vrf(wg['vrf']) - - # remove peers - for pub_key in wg['peer_remove']: - w.remove_peer(pub_key) - - # peer pubkey - # setting up the wg interface - w.config['private_key'] = c['pk'] - - for peer in wg['peer']: - # peer pubkey - w.config['pubkey'] = peer['pubkey'] - # peer allowed-ips - w.config['allowed-ips'] = peer['allowed-ips'] - # local listen port - if wg['listen_port']: - w.config['port'] = wg['listen_port'] - # fwmark - if c['fwmark']: - w.config['fwmark'] = wg['fwmark'] - - # endpoint - if peer['address'] and peer['port']: - if is_ipv6(peer['address']): - w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port']) - else: - w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port']) - - # persistent-keepalive - if peer['persistent_keepalive']: - w.config['keepalive'] = peer['persistent_keepalive'] - - if peer['psk']: - w.config['psk'] = peer['psk'] - - w.update() - - # Enable/Disable interface - if wg['disable']: - w.set_admin_state('down') - else: - w.set_admin_state('up') - + w = WireGuardIf(wireguard['ifname']) + w.update(wireguard) return None if __name__ == '__main__': try: - _check_kmod() - _migrate_default_keys() + check_kmod('wireguard') c = get_config() verify(c) apply(c) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 0162b642c..9861f72db 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -15,497 +15,166 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os + from sys import exit from re import findall - from copy import deepcopy - -from netifaces import interfaces from netaddr import EUI, mac_unix_expanded from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import WiFiIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import dict_merge +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import WiFiIf from vyos.template import render -from vyos.util import chown, call -from vyos.validate import is_member +from vyos.util import call from vyos import ConfigError - from vyos import airbag airbag.enable() -default_config_data = { - **interface_default_data, - 'cap_ht' : False, - 'cap_ht_40mhz_incapable' : False, - 'cap_ht_powersave' : False, - 'cap_ht_chan_set_width' : '', - 'cap_ht_delayed_block_ack' : False, - 'cap_ht_dsss_cck_40' : False, - 'cap_ht_greenfield' : False, - 'cap_ht_ldpc' : False, - 'cap_ht_lsig_protection' : False, - 'cap_ht_max_amsdu' : '', - 'cap_ht_short_gi' : [], - 'cap_ht_smps' : '', - 'cap_ht_stbc_rx' : '', - 'cap_ht_stbc_tx' : False, - 'cap_req_ht' : False, - 'cap_req_vht' : False, - 'cap_vht' : False, - 'cap_vht_antenna_cnt' : '', - 'cap_vht_antenna_fixed' : False, - 'cap_vht_beamform' : '', - 'cap_vht_center_freq_1' : '', - 'cap_vht_center_freq_2' : '', - 'cap_vht_chan_set_width' : '', - 'cap_vht_ldpc' : False, - 'cap_vht_link_adaptation' : '', - 'cap_vht_max_mpdu_exp' : '', - 'cap_vht_max_mpdu' : '', - 'cap_vht_short_gi' : [], - 'cap_vht_stbc_rx' : '', - 'cap_vht_stbc_tx' : False, - 'cap_vht_tx_powersave' : False, - 'cap_vht_vht_cf' : False, - 'channel': '', - 'country_code': '', - 'deleted': False, - 'disable_broadcast_ssid' : False, - 'disable_link_detect' : 1, - 'expunge_failing_stations' : False, - 'hw_id' : '', - 'intf': '', - 'isolate_stations' : False, - 'max_stations' : '', - 'mgmt_frame_protection' : 'disabled', - 'mode' : 'g', - 'phy' : '', - 'reduce_transmit_power' : '', - 'sec_wep' : False, - 'sec_wep_key' : [], - 'sec_wpa' : False, - 'sec_wpa_cipher' : [], - 'sec_wpa_mode' : 'both', - 'sec_wpa_passphrase' : '', - 'sec_wpa_radius' : [], - 'ssid' : '', - 'op_mode' : 'monitor', - 'vif': {}, - 'vif_remove': [], - 'vif_s': {}, - 'vif_s_remove': [] -} - # XXX: wpa_supplicant works on the source interface -wpa_suppl_conf = '/run/wpa_supplicant/{intf}.conf' -hostapd_conf = '/run/hostapd/{intf}.conf' - -def get_config(): - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - conf = Config() - - # check if wireless interface has been removed - cfg_base = ['interfaces', 'wireless ', ifname] - if not conf.exists(cfg_base): - wifi = deepcopy(default_config_data) - wifi['intf'] = ifname - wifi['deleted'] = True - # we need to know if we're a bridge member so we can refuse deletion - wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge') - # we can not bail out early as wireless interface can not be removed - # Kernel will complain with: RTNETLINK answers: Operation not supported. - # Thus we need to remove individual settings - return wifi - - # set new configuration level - conf.set_level(cfg_base) - - # get common interface settings - wifi, disabled = intf_to_dict(conf, default_config_data) - - # 40MHz intolerance, use 20MHz only - if conf.exists('capabilities ht 40mhz-incapable'): - wifi['cap_ht'] = True - wifi['cap_ht_40mhz_incapable'] = True - - # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] - if conf.exists('capabilities ht auto-powersave'): - wifi['cap_ht'] = True - wifi['cap_ht_powersave'] = True - - # Supported channel set width - if conf.exists('capabilities ht channel-set-width'): - wifi['cap_ht'] = True - wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width') - - # HT-delayed Block Ack - if conf.exists('capabilities ht delayed-block-ack'): - wifi['cap_ht'] = True - wifi['cap_ht_delayed_block_ack'] = True - - # DSSS/CCK Mode in 40 MHz - if conf.exists('capabilities ht dsss-cck-40'): - wifi['cap_ht'] = True - wifi['cap_ht_dsss_cck_40'] = True - - # HT-greenfield capability - if conf.exists('capabilities ht greenfield'): - wifi['cap_ht'] = True - wifi['cap_ht_greenfield'] = True - - # LDPC coding capability - if conf.exists('capabilities ht ldpc'): - wifi['cap_ht'] = True - wifi['cap_ht_ldpc'] = True - - # L-SIG TXOP protection capability - if conf.exists('capabilities ht lsig-protection'): - wifi['cap_ht'] = True - wifi['cap_ht_lsig_protection'] = True - - # Set Maximum A-MSDU length - if conf.exists('capabilities ht max-amsdu'): - wifi['cap_ht'] = True - wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu') - - # Short GI capabilities - if conf.exists('capabilities ht short-gi'): - wifi['cap_ht'] = True - wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi') - - # Spatial Multiplexing Power Save (SMPS) settings - if conf.exists('capabilities ht smps'): - wifi['cap_ht'] = True - wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps') - - # Support for receiving PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities ht stbc rx'): - wifi['cap_ht'] = True - wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx') - - # Support for sending PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities ht stbc tx'): - wifi['cap_ht'] = True - wifi['cap_ht_stbc_tx'] = True - - # Require stations to support HT PHY (reject association if they do not) - if conf.exists('capabilities require-ht'): - wifi['cap_req_ht'] = True - - # Require stations to support VHT PHY (reject association if they do not) - if conf.exists('capabilities require-vht'): - wifi['cap_req_vht'] = True - - # Number of antennas on this card - if conf.exists('capabilities vht antenna-count'): - wifi['cap_vht'] = True - wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count') - - # set if antenna pattern does not change during the lifetime of an association - if conf.exists('capabilities vht antenna-pattern-fixed'): - wifi['cap_vht'] = True - wifi['cap_vht_antenna_fixed'] = True - - # Beamforming capabilities - if conf.exists('capabilities vht beamform'): - wifi['cap_vht'] = True - wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform') - - # VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes) - if conf.exists('capabilities vht center-channel-freq freq-1'): - wifi['cap_vht'] = True - wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1') - - # VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode) - if conf.exists('capabilities vht center-channel-freq freq-2'): - wifi['cap_vht'] = True - wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2') - - # VHT operating Channel width - if conf.exists('capabilities vht channel-set-width'): - wifi['cap_vht'] = True - wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width') - - # LDPC coding capability - if conf.exists('capabilities vht ldpc'): - wifi['cap_vht'] = True - wifi['cap_vht_ldpc'] = True - - # VHT link adaptation capabilities - if conf.exists('capabilities vht link-adaptation'): - wifi['cap_vht'] = True - wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation') - - # Set the maximum length of A-MPDU pre-EOF padding that the station can receive - if conf.exists('capabilities vht max-mpdu-exp'): - wifi['cap_vht'] = True - wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp') - - # Increase Maximum MPDU length - if conf.exists('capabilities vht max-mpdu'): - wifi['cap_vht'] = True - wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu') - - # Increase Maximum MPDU length - if conf.exists('capabilities vht short-gi'): - wifi['cap_vht'] = True - wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi') - - # Support for receiving PPDU using STBC (Space Time Block Coding) - if conf.exists('capabilities vht stbc rx'): - wifi['cap_vht'] = True - wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx') - - # Support for the transmission of at least 2x1 STBC (Space Time Block Coding) - if conf.exists('capabilities vht stbc tx'): - wifi['cap_vht'] = True - wifi['cap_vht_stbc_tx'] = True - - # Support for VHT TXOP Power Save Mode - if conf.exists('capabilities vht tx-powersave'): - wifi['cap_vht'] = True - wifi['cap_vht_tx_powersave'] = True - - # STA supports receiving a VHT variant HT Control field - if conf.exists('capabilities vht vht-cf'): - wifi['cap_vht'] = True - wifi['cap_vht_vht_cf'] = True - - # Wireless radio channel - if conf.exists('channel'): - wifi['channel'] = conf.return_value('channel') - - # Disable broadcast of SSID from access-point - if conf.exists('disable-broadcast-ssid'): - wifi['disable_broadcast_ssid'] = True - - # Disassociate stations based on excessive transmission failures - if conf.exists('expunge-failing-stations'): - wifi['expunge_failing_stations'] = True - - # retrieve real hardware address - if conf.exists('hw-id'): - wifi['hw_id'] = conf.return_value('hw-id') - - # Isolate stations on the AP so they cannot see each other - if conf.exists('isolate-stations'): - wifi['isolate_stations'] = True - - # Wireless physical device - if conf.exists('physical-device'): - wifi['phy'] = conf.return_value('physical-device') - - # Maximum number of wireless radio stations - if conf.exists('max-stations'): - wifi['max_stations'] = conf.return_value('max-stations') - - # Management Frame Protection (MFP) according to IEEE 802.11w - if conf.exists('mgmt-frame-protection'): - wifi['mgmt_frame_protection'] = conf.return_value('mgmt-frame-protection') - - # Wireless radio mode - if conf.exists('mode'): - wifi['mode'] = conf.return_value('mode') - - # Transmission power reduction in dBm - if conf.exists('reduce-transmit-power'): - wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power') - - # WEP enabled? - if conf.exists('security wep'): - wifi['sec_wep'] = True - - # WEP encryption key(s) - if conf.exists('security wep key'): - wifi['sec_wep_key'] = conf.return_values('security wep key') - - # WPA enabled? - if conf.exists('security wpa'): - wifi['sec_wpa'] = True - - # WPA Cipher suite - if conf.exists('security wpa cipher'): - wifi['sec_wpa_cipher'] = conf.return_values('security wpa cipher') - - # WPA mode - if conf.exists('security wpa mode'): - wifi['sec_wpa_mode'] = conf.return_value('security wpa mode') - - # WPA default ciphers depend on WPA mode - if not wifi['sec_wpa_cipher']: - if wifi['sec_wpa_mode'] == 'wpa': - wifi['sec_wpa_cipher'].append('TKIP') - wifi['sec_wpa_cipher'].append('CCMP') - - elif wifi['sec_wpa_mode'] == 'wpa2': - wifi['sec_wpa_cipher'].append('CCMP') - - elif wifi['sec_wpa_mode'] == 'both': - wifi['sec_wpa_cipher'].append('CCMP') - wifi['sec_wpa_cipher'].append('TKIP') - - # WPA Group Cipher suite - if conf.exists('security wpa group-cipher'): - wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher') - - # WPA personal shared pass phrase - if conf.exists('security wpa passphrase'): - wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') - - # WPA RADIUS source address - if conf.exists('security wpa radius source-address'): - wifi['sec_wpa_radius_source'] = conf.return_value('security wpa radius source-address') - - # WPA RADIUS server - for server in conf.list_nodes('security wpa radius server'): - # set new configuration level - conf.set_level(cfg_base + ' security wpa radius server ' + server) - radius = { - 'server' : server, - 'acc_port' : '', - 'disabled': False, - 'port' : 1812, - 'key' : '' - } - - # RADIUS server port - if conf.exists('port'): - radius['port'] = int(conf.return_value('port')) - - # receive RADIUS accounting info - if conf.exists('accounting'): - radius['acc_port'] = radius['port'] + 1 - - # Check if RADIUS server was temporary disabled - if conf.exists(['disable']): - radius['disabled'] = True - - # RADIUS server shared-secret - if conf.exists('key'): - radius['key'] = conf.return_value('key') - - # append RADIUS server to list of servers - wifi['sec_wpa_radius'].append(radius) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - - # Wireless access-point service set identifier (SSID) - if conf.exists('ssid'): - wifi['ssid'] = conf.return_value('ssid') - - # Wireless device type for this interface - if conf.exists('type'): - tmp = conf.return_value('type') - if tmp == 'access-point': - tmp = 'ap' - - wifi['op_mode'] = tmp +wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' +hostapd_conf = '/run/hostapd/{ifname}.conf' + +def find_other_stations(conf, base, ifname): + """ + Only one wireless interface per phy can be in station mode - + find all interfaces attached to a phy which run in station mode + """ + old_level = conf.get_level() + conf.set_level(base) + dict = {} + for phy in os.listdir('/sys/class/ieee80211'): + list = [] + for interface in conf.list_nodes([]): + if interface == ifname: + continue + # the following node is mandatory + if conf.exists([interface, 'physical-device', phy]): + tmp = conf.return_value([interface, 'type']) + if tmp == 'station': + list.append(interface) + if list: + dict.update({phy: list}) + conf.set_level(old_level) + return dict + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'wireless'] + wifi = get_interface_dict(conf, base) + + if 'security' in wifi and 'wpa' in wifi['security']: + wpa_cipher = wifi['security']['wpa'].get('cipher') + wpa_mode = wifi['security']['wpa'].get('mode') + if not wpa_cipher: + tmp = None + if wpa_mode == 'wpa': + tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}} + elif wpa_mode == 'wpa2': + tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}} + elif wpa_mode == 'both': + tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}} + + if tmp: wifi = dict_merge(tmp, wifi) # retrieve configured regulatory domain - conf.set_level('system') - if conf.exists('wifi-regulatory-domain'): - wifi['country_code'] = conf.return_value('wifi-regulatory-domain') + conf.set_level(['system']) + if conf.exists(['wifi-regulatory-domain']): + wifi['country_code'] = conf.return_value(['wifi-regulatory-domain']) - return wifi + # Only one wireless interface per phy can be in station mode + tmp = find_other_stations(conf, base, wifi['ifname']) + if tmp: wifi['station_interfaces'] = tmp + return wifi def verify(wifi): - if wifi['deleted']: - if wifi['is_bridge_member']: - raise ConfigError(( - f'Cannot delete interface "{wifi["intf"]}" as it is a ' - f'member of bridge "{wifi["is_bridge_member"]}"!')) - + if 'deleted' in wifi: + verify_bridge_delete(wifi) return None - if wifi['op_mode'] != 'monitor' and not wifi['ssid']: - raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) + if 'physical_device' not in wifi: + raise ConfigError('You must specify a physical-device "phy"') - if not wifi['phy']: - raise ConfigError('You must specify physical-device') - - if not wifi['mode']: + if 'type' not in wifi: raise ConfigError('You must specify a WiFi mode') - if wifi['op_mode'] == 'ap': - c = Config() - if not c.exists('system wifi-regulatory-domain'): - raise ConfigError('Wireless regulatory domain is mandatory,\n' \ - 'use "set system wifi-regulatory-domain".') - - if not wifi['channel']: - raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - - if len(wifi['sec_wep_key']) > 4: - raise ConfigError('No more then 4 WEP keys configurable') + if 'ssid' not in wifi and wifi['type'] != 'monitor': + raise ConfigError('SSID must be configured') - if wifi['cap_vht'] and not wifi['cap_ht']: - raise ConfigError('Specify HT flags if you want to use VHT!') - - if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: - raise ConfigError('Cannot use beam forming with just one antenna!') - - if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3: - # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 - raise ConfigError('Single-user beam former requires at least 3 antennas!') - - if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): - raise ConfigError('Missing WEP keys') - - if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']): - raise ConfigError('Misssing WPA key or RADIUS server') - - for radius in wifi['sec_wpa_radius']: - if not radius['key']: - raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) - - if ( wifi['is_bridge_member'] - and ( wifi['address'] - or wifi['ipv6_eui64_prefix'] - or wifi['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{wifi["intf"]}" ' - f'as it is a member of bridge "{wifi["is_bridge_member"]}"!')) - - if wifi['vrf']: - if wifi['vrf'] not in interfaces(): - raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist') - - if wifi['is_bridge_member']: - raise ConfigError(( - f'Interface "{wifi["intf"]}" cannot be member of VRF ' - f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} ' - f'at the same time!')) + if wifi['type'] == 'access-point': + if 'country_code' not in wifi: + raise ConfigError('Wireless regulatory domain is mandatory,\n' \ + 'use "set system wifi-regulatory-domain" for configuration.') + + if 'channel' not in wifi: + raise ConfigError('Wireless channel must be configured!') + + if 'security' in wifi: + if {'wep', 'wpa'} <= set(wifi.get('security', {})): + raise ConfigError('Must either use WEP or WPA security!') + + if 'wep' in wifi['security']: + if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4: + raise ConfigError('No more then 4 WEP keys configurable') + elif 'key' not in wifi['security']['wep']: + raise ConfigError('Security WEP configured - missing WEP keys!') + + elif 'wpa' in wifi['security']: + wpa = wifi['security']['wpa'] + if not any(i in ['passphrase', 'radius'] for i in wpa): + raise ConfigError('Misssing WPA key or RADIUS server') + + if 'radius' in wpa: + if 'server' in wpa['radius']: + for server in wpa['radius']['server']: + if 'key' not in wpa['radius']['server'][server]: + raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}') + + if 'capabilities' in wifi: + capabilities = wifi['capabilities'] + if 'vht' in capabilities: + if 'ht' not in capabilities: + raise ConfigError('Specify HT flags if you want to use VHT!') + + if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})): + if capabilities['vht']['antenna_count'] == '1': + raise ConfigError('Cannot use beam forming with just one antenna!') + + if capabilities['vht']['beamform'] == 'single-user-beamformer': + if int(capabilities['vht']['antenna_count']) < 3: + # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 + raise ConfigError('Single-user beam former requires at least 3 antennas!') + + if 'station_interfaces' in wifi and wifi['type'] == 'station': + phy = wifi['physical_device'] + if phy in wifi['station_interfaces']: + if len(wifi['station_interfaces'][phy]) > 0: + raise ConfigError('Only one station per wireless physical interface possible!') + + verify_address(wifi) + verify_vrf(wifi) # use common function to verify VLAN configuration verify_vlan_config(wifi) - conf = Config() - # Only one wireless interface per phy can be in station mode - base = ['interfaces', 'wireless'] - for phy in os.listdir('/sys/class/ieee80211'): - stations = [] - for wlan in conf.list_nodes(base): - # the following node is mandatory - if conf.exists(base + [wlan, 'physical-device', phy]): - tmp = conf.return_value(base + [wlan, 'type']) - if tmp == 'station': - stations.append(wlan) - - if len(stations) > 1: - raise ConfigError('Only one station per wireless physical interface possible!') - return None def generate(wifi): - interface = wifi['intf'] + interface = wifi['ifname'] # always stop hostapd service first before reconfiguring it call(f'systemctl stop hostapd@{interface}.service') @@ -513,7 +182,7 @@ def generate(wifi): call(f'systemctl stop wpa_supplicant@{interface}.service') # Delete config files if interface is removed - if wifi['deleted']: + if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): os.unlink(hostapd_conf.format(**wifi)) @@ -522,10 +191,10 @@ def generate(wifi): return None - if not wifi['mac']: + if 'mac' not in wifi: # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd # generate locally administered MAC address from used phy interface - with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f: + with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f: # some PHYs tend to have multiple interfaces and thus supply multiple MAC # addresses - we only need the first one for our calculation tmp = f.readline().rstrip() @@ -545,20 +214,18 @@ def generate(wifi): wifi['mac'] = str(mac) # render appropriate new config files depending on access-point or station mode - if wifi['op_mode'] == 'ap': - render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi) + if wifi['type'] == 'access-point': + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) - elif wifi['op_mode'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi) + elif wifi['type'] == 'station': + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True) return None def apply(wifi): - interface = wifi['intf'] - if wifi['deleted']: - w = WiFiIf(interface) - # delete interface - w.remove() + interface = wifi['ifname'] + if 'deleted' in wifi: + WiFiIf(interface).remove() else: # WiFi interface needs to be created on-block (e.g. mode or physical # interface) instead of passing a ton of arguments, I just use a dict @@ -566,97 +233,21 @@ def apply(wifi): conf = deepcopy(WiFiIf.get_config()) # Assign WiFi instance configuration parameters to config dict - conf['phy'] = wifi['phy'] + conf['phy'] = wifi['physical_device'] # Finally create the new interface w = WiFiIf(interface, **conf) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not wifi['is_bridge_member']: - w.set_vrf(wifi['vrf']) - - # update interface description used e.g. within SNMP - w.set_alias(wifi['description']) - - if wifi['dhcp_client_id']: - w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id'] - - if wifi['dhcp_hostname']: - w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname'] - - if wifi['dhcp_vendor_class_id']: - w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id'] - - if wifi['dhcpv6_prm_only']: - w.dhcp.v6.options['dhcpv6_prm_only'] = True - - if wifi['dhcpv6_temporary']: - w.dhcp.v6.options['dhcpv6_temporary'] = True - - if wifi['dhcpv6_pd_length']: - w.dhcp.v6.options['dhcpv6_pd_length'] = wifi['dhcpv6_pd_length'] - - if wifi['dhcpv6_pd_interfaces']: - w.dhcp.v6.options['dhcpv6_pd_interfaces'] = wifi['dhcpv6_pd_interfaces'] - - # ignore link state changes - w.set_link_detect(wifi['disable_link_detect']) - - # Delete old IPv6 EUI64 addresses before changing MAC - for addr in wifi['ipv6_eui64_prefix_remove']: - w.del_ipv6_eui64_address(addr) - - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed - if wifi['mac']: - w.set_mac(wifi['mac']) - elif wifi['hw_id']: - w.set_mac(wifi['hw_id']) - - # Add IPv6 EUI-based addresses - for addr in wifi['ipv6_eui64_prefix']: - w.add_ipv6_eui64_address(addr) - - # configure ARP filter configuration - w.set_arp_filter(wifi['ip_disable_arp_filter']) - # configure ARP accept - w.set_arp_accept(wifi['ip_enable_arp_accept']) - # configure ARP announce - w.set_arp_announce(wifi['ip_enable_arp_announce']) - # configure ARP ignore - w.set_arp_ignore(wifi['ip_enable_arp_ignore']) - # IPv6 accept RA - w.set_ipv6_accept_ra(wifi['ipv6_accept_ra']) - # IPv6 address autoconfiguration - w.set_ipv6_autoconf(wifi['ipv6_autoconf']) - # IPv6 forwarding - w.set_ipv6_forwarding(wifi['ipv6_forwarding']) - # IPv6 Duplicate Address Detection (DAD) tries - w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect']) - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in wifi['address_remove']: - w.del_addr(addr) - for addr in wifi['address']: - w.add_addr(addr) - - # apply all vlans to interface - apply_all_vlans(w, wifi) + w.update(wifi) # Enable/Disable interface - interface is always placed in # administrative down state in WiFiIf class - if not wifi['disable']: - w.set_admin_state('up') - + if 'disable' not in wifi: # Physical interface is now configured. Proceed by starting hostapd or # wpa_supplicant daemon. When type is monitor we can just skip this. - if wifi['op_mode'] == 'ap': + if wifi['type'] == 'access-point': call(f'systemctl start hostapd@{interface}.service') - elif wifi['op_mode'] == 'station': + elif wifi['type'] == 'station': call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index ec5a85e54..7d8110096 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -16,75 +16,42 @@ import os -from fnmatch import fnmatch from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.template import render from vyos.util import call -from vyos.xml import defaults +from vyos.util import check_kmod +from vyos.util import find_device_file from vyos import ConfigError from vyos import airbag airbag.enable() -def check_kmod(): - modules = ['option', 'usb_wwan', 'usbserial'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - -def find_device_file(device): - """ Recurively search /dev for the given device file and return its full path. - If no device file was found 'None' is returned """ - for root, dirs, files in os.walk('/dev'): - for basename in files: - if fnmatch(basename, device): - return os.path.join(root, basename) +k_mod = ['option', 'usb_wwan', 'usbserial'] - return None - -def get_config(): - """ Retrive CLI config as dictionary. Dictionary can never be empty, - as at least the interface name will be added or a deleted flag """ - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - # retrieve interface default values +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() base = ['interfaces', 'wirelessmodem'] - default_values = defaults(base) - - ifname = os.environ['VYOS_TAGNODE_VALUE'] - base = base + [ifname] - - wwan = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Check if interface has been removed - if wwan == {}: - wwan.update({'deleted' : ''}) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - wwan = dict_merge(default_values, wwan) - - # Add interface instance name into dictionary - wwan.update({'ifname': ifname}) - + wwan = get_interface_dict(conf, base) return wwan def verify(wwan): - if 'deleted' in wwan.keys(): + if 'deleted' in wwan: return None - if not 'apn' in wwan.keys(): + if not 'apn' in wwan: raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) - if not 'device' in wwan.keys(): + if not 'device' in wwan: raise ConfigError('Physical "device" must be configured') # we can not use isfile() here as Linux device files are no regular files @@ -141,11 +108,11 @@ def generate(wwan): return None def apply(wwan): - if 'deleted' in wwan.keys(): + if 'deleted' in wwan: # bail out early return None - if not 'disable' in wwan.keys(): + if not 'disable' in wwan: # "dial" WWAN connection call('systemctl start ppp@{ifname}.service'.format(**wwan)) @@ -153,7 +120,7 @@ def apply(wwan): if __name__ == '__main__': try: - check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 015d1a480..11a5b7aaa 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -41,8 +41,11 @@ delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" charon_pidfile = "/var/run/charon.pid" -def get_config(): - config = Config() +def get_config(config=None): + if config: + config = config + else: + config = Config() data = {"install_routes": "yes"} if config.exists("vpn ipsec options disable-route-autoinstall"): diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index 5b965f95f..755c89966 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -27,6 +27,7 @@ from vyos import airbag airbag.enable() vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] +vyos_certbot_dir = vyos.defaults.directories['certbot'] dependencies = [ 'https.py', @@ -45,7 +46,7 @@ def request_certbot(cert): else: domain_flag = '' - certbot_cmd = 'certbot certonly -n --nginx --agree-tos --no-eff-email --expand {0} {1}'.format(email_flag, domain_flag) + certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}' cmd(certbot_cmd, raising=ConfigError, diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index 1b539887a..6b645857a 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -146,9 +146,12 @@ def get_location(config): return intfs_location -def get_config(): +def get_config(config=None): lldp = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists(base): return None else: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 3dd20938a..eb634fd78 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -19,18 +19,27 @@ import json import os from copy import deepcopy +from distutils.version import LooseVersion +from platform import release as kernel_version from sys import exit from netifaces import interfaces from vyos.config import Config from vyos.template import render -from vyos.util import call, cmd +from vyos.util import call +from vyos.util import cmd +from vyos.util import check_kmod from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() +if LooseVersion(kernel_version()) > LooseVersion('5.1'): + k_mod = ['nft_nat', 'nft_chain_nat'] +else: + k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] + default_config_data = { 'deleted': False, 'destination': [], @@ -44,15 +53,6 @@ default_config_data = { iptables_nat_config = '/tmp/vyos-nat-rules.nft' -def _check_kmod(): - """ load required Kernel modules """ - modules = ['nft_nat', 'nft_chain_nat_ipv4'] - for module in modules: - if not os.path.exists(f'/sys/module/{module}'): - if call(f'modprobe {module}') != 0: - raise ConfigError(f'Loading Kernel module {module} failed') - - def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. Handler is required when adding NAT/Conntrack helper targets """ @@ -79,9 +79,6 @@ def verify_rule(rule, err_msg): 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') - if not rule['exclude'] and not rule['translation_address']: - raise ConfigError(f'{err_msg} translation address not specified') - def parse_configuration(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ @@ -170,9 +167,12 @@ def parse_configuration(conf, source_dest): return ruleset -def get_config(): +def get_config(config=None): nat = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() # read in current nftable (once) for further processing tmp = cmd('nft -j list table raw') @@ -240,6 +240,8 @@ def verify(nat): addr = rule['translation_address'] if addr != 'masquerade' and not is_addr_assigned(addr): print(f'Warning: IP address {addr} does not exist on the system!') + elif not rule['exclude']: + raise ConfigError(f'{err_msg} translation address not specified') # common rule verification verify_rule(rule, err_msg) @@ -254,6 +256,9 @@ def verify(nat): if not rule['interface_in']: raise ConfigError(f'{err_msg} inbound-interface not specified') + if not rule['translation_address'] and not rule['exclude']: + raise ConfigError(f'{err_msg} translation address not specified') + # common rule verification verify_rule(rule, err_msg) @@ -272,7 +277,7 @@ def apply(nat): if __name__ == '__main__': try: - _check_kmod() + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index bba8f87a4..d6453ec83 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -27,8 +27,11 @@ airbag.enable() config_file = r'/etc/ntp.conf' systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['system', 'ntp'] ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 3aa76d866..1978adff5 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -14,83 +14,72 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import jmespath +import os -from copy import deepcopy from sys import exit from vyos.config import Config +from vyos.util import call from vyos.template import render +from vyos.template import render_to_string +from vyos import frr from vyos import ConfigError, airbag airbag.enable() config_file = r'/tmp/bgp.frr' -default_config_data = { - 'as_number': '' -} - def get_config(): - bgp = deepcopy(default_config_data) conf = Config() - - # this lives in the "nbgp" tree until we switch over base = ['protocols', 'nbgp'] + bgp = conf.get_config_dict(base, key_mangling=('-', '_')) if not conf.exists(base): return None - bgp = deepcopy(default_config_data) - # Get full BGP configuration as dictionary - output the configuration for development - # - # vyos@vyos# commit - # [ protocols nbgp 65000 ] - # {'nbgp': {'65000': {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {}, - # '2.2.2.0/24': {}}}, - # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}}, - # 'neighbor': {'192.0.2.1': {'password': 'foo', - # 'remote-as': '100'}}}}} - # - tmp = conf.get_config_dict(base) - - # extract base key from dict as this is our AS number - bgp['as_number'] = jmespath.search('nbgp | keys(@) [0]', tmp) - - # adjust level of dictionary returned by get_config_dict() - # by using jmesgpath and update dictionary - bgp.update(jmespath.search('nbgp.* | [0]', tmp)) - from pprint import pprint pprint(bgp) - # resulting in e.g. - # vyos@vyos# commit - # [ protocols nbgp 65000 ] - # {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {}, - # '2.2.2.0/24': {}}}, - # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}}, - # 'as_number': '65000', - # 'neighbor': {'192.0.2.1': {'password': 'foo', 'remote-as': '100'}}, - # 'timers': {'holdtime': '5'}} return bgp def verify(bgp): - # bail out early - looks like removal from running config if not bgp: return None return None def generate(bgp): - # bail out early - looks like removal from running config if not bgp: return None + # render(config) not needed, its only for debug render(config_file, 'frr/bgp.frr.tmpl', bgp) + + bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp) + return None def apply(bgp): + if bgp is None: + return None + + # Save original configration prior to starting any commit actions + bgp['original_config'] = frr.get_configuration(daemon='bgpd') + bgp['modified_config'] = frr.replace_section(bgp['original_config'], bgp['new_frr_config'], from_re='router bgp .*') + + # Debugging + print('--------- DEBUGGING ----------') + print(f'Existing config:\n{bgp["original_config"]}\n\n') + print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n') + print(f'Modified config:\n{bgp["modified_config"]}\n\n') + + # Frr Mark configuration will test for syntax errors and exception out if any syntax errors are detected + frr.mark_configuration(bgp['modified_config']) + + # Commit the resulting new configuration to frr, this will render an frr.CommitError() Exception on fail + frr.reload_configuration(bgp['modified_config'], daemon='bgpd') + return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index ca148fd6a..6f4fc784d 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -29,8 +29,11 @@ airbag.enable() config_file = r'/tmp/igmp.frr' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() igmp_conf = { 'igmp_conf' : False, 'old_ifaces' : {}, diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 72208ffa1..e515490d0 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -29,8 +29,11 @@ config_file = r'/tmp/ldpd.frr' def sysctl(name, value): call('sysctl -wq {}={}'.format(name, value)) -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() mpls_conf = { 'router_id' : None, 'mpls_ldp' : False, @@ -38,13 +41,17 @@ def get_config(): 'interfaces' : [], 'neighbors' : {}, 'd_transp_ipv4' : None, - 'd_transp_ipv6' : None + 'd_transp_ipv6' : None, + 'hello_holdtime' : None, + 'hello_interval' : None }, 'ldp' : { 'interfaces' : [], 'neighbors' : {}, 'd_transp_ipv4' : None, - 'd_transp_ipv6' : None + 'd_transp_ipv6' : None, + 'hello_holdtime' : None, + 'hello_interval' : None } } if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')): @@ -61,6 +68,20 @@ def get_config(): if conf.exists('router-id'): mpls_conf['router_id'] = conf.return_value('router-id') + # Get hello holdtime + if conf.exists_effective('discovery hello-holdtime'): + mpls_conf['old_ldp']['hello_holdtime'] = conf.return_effective_value('discovery hello-holdtime') + + if conf.exists('discovery hello-holdtime'): + mpls_conf['ldp']['hello_holdtime'] = conf.return_value('discovery hello-holdtime') + + # Get hello interval + if conf.exists_effective('discovery hello-interval'): + mpls_conf['old_ldp']['hello_interval'] = conf.return_effective_value('discovery hello-interval') + + if conf.exists('discovery hello-interval'): + mpls_conf['ldp']['hello_interval'] = conf.return_value('discovery hello-interval') + # Get discovery transport-ipv4-address if conf.exists_effective('discovery transport-ipv4-address'): mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address') diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 8aa324bac..6d333e19a 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -29,8 +29,11 @@ airbag.enable() config_file = r'/tmp/pimd.frr' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() pim_conf = { 'pim_conf' : False, 'old_pim' : { diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 4f8816d61..8ddd705f2 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -28,8 +28,11 @@ airbag.enable() config_file = r'/tmp/ripd.frr' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['protocols', 'rip'] rip_conf = { 'rip_conf' : False, @@ -97,7 +100,7 @@ def get_config(): # Get distribute list interface old_rip for dist_iface in conf.list_effective_nodes('distribute-list interface'): # Set level 'distribute-list interface ethX' - conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + conf.set_level(base + ['distribute-list', 'interface', dist_iface]) rip_conf['rip']['distribute'].update({ dist_iface : { 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)), @@ -125,7 +128,7 @@ def get_config(): # Get distribute list interface for dist_iface in conf.list_nodes('distribute-list interface'): # Set level 'distribute-list interface ethX' - conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + conf.set_level(base + ['distribute-list', 'interface', dist_iface]) rip_conf['rip']['distribute'].update({ dist_iface : { 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)), @@ -148,7 +151,7 @@ def get_config(): if conf.exists('prefix-list out'.format(dist_iface)): rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface)) - conf.set_level((str(base)) + ' distribute-list') + conf.set_level(base + ['distribute-list']) # Get distribute list, access-list in if conf.exists_effective('access-list in'): diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py index 232d1e181..99157835a 100755 --- a/src/conf_mode/protocols_static_multicast.py +++ b/src/conf_mode/protocols_static_multicast.py @@ -30,8 +30,11 @@ airbag.enable() config_file = r'/tmp/static_mcast.frr' # Get configuration for static multicast route -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() mroute = { 'old_mroute' : {}, 'mroute' : {} diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index 3343d1247..841bf6a39 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -44,9 +44,12 @@ default_config_data = { 'master_key': '' } -def get_config(): +def get_config(config=None): salt = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base = ['service', 'salt-minion'] if not conf.exists(base): diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index ace6b8ca4..0e5fc75b0 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -27,15 +27,16 @@ from vyos import ConfigError config_file = r'/run/conserver/conserver.cf' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['service', 'console-server'] - if not conf.exists(base): - return None - # Retrieve CLI representation as dictionary - proxy = conf.get_config_dict(base, key_mangling=('-', '_')) + proxy = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # The retrieved dictionary will look something like this: # # {'device': {'usb0b2.4p1.0': {'speed': '9600'}, @@ -47,9 +48,10 @@ def get_config(): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base + ['device']) - for device in proxy['device'].keys(): - tmp = dict_merge(default_values, proxy['device'][device]) - proxy['device'][device] = tmp + if 'device' in proxy: + for device in proxy['device']: + tmp = dict_merge(default_values, proxy['device'][device]) + proxy['device'][device] = tmp return proxy @@ -57,15 +59,14 @@ def verify(proxy): if not proxy: return None - for device in proxy['device']: - keys = proxy['device'][device].keys() - if 'speed' not in keys: - raise ConfigError(f'Serial port speed must be defined for "{tmp}"!') + if 'device' in proxy: + for device in proxy['device']: + if 'speed' not in proxy['device'][device]: + raise ConfigError(f'Serial port speed must be defined for "{device}"!') - if 'ssh' in keys: - ssh_keys = proxy['device'][device]['ssh'].keys() - if 'port' not in ssh_keys: - raise ConfigError(f'SSH port must be defined for "{tmp}"!') + if 'ssh' in proxy['device'][device]: + if 'port' not in proxy['device'][device]['ssh']: + raise ConfigError(f'SSH port must be defined for "{device}"!') return None @@ -86,10 +87,11 @@ def apply(proxy): call('systemctl restart conserver-server.service') - for device in proxy['device']: - if 'ssh' in proxy['device'][device].keys(): - port = proxy['device'][device]['ssh']['port'] - call(f'systemctl restart dropbear@{device}.service') + if 'device' in proxy: + for device in proxy['device']: + if 'ssh' in proxy['device'][device]: + port = proxy['device'][device]['ssh']['port'] + call(f'systemctl restart dropbear@{device}.service') return None diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index d46f9578e..27d0ee60c 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -28,8 +28,11 @@ airbag.enable() config_file = r'/etc/fastnetmon.conf' networks_list = r'/etc/networks_list' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['service', 'ids', 'ddos-protection'] fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return fastnetmon diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index b539da98e..96cf932d1 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -55,8 +55,11 @@ default_config_data = { 'thread_cnt': get_half_cpus() } -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base_path = ['service', 'ipoe-server'] if not conf.exists(base_path): return None @@ -147,17 +150,21 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/service_mdns-repeater.py index b43f9bdd8..729518c96 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -17,69 +17,54 @@ import os from sys import exit -from copy import deepcopy -from netifaces import ifaddresses, AF_INET +from netifaces import ifaddresses, interfaces, AF_INET from vyos.config import Config -from vyos import ConfigError -from vyos.util import call from vyos.template import render - +from vyos.util import call +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/default/mdns-repeater' -default_config_data = { - 'disabled': False, - 'interfaces': [] -} - -def get_config(): - mdns = deepcopy(default_config_data) - conf = Config() - base = ['service', 'mdns', 'repeater'] - if not conf.exists(base): - return None +def get_config(config=None): + if config: + conf = config else: - conf.set_level(base) - - # Service can be disabled by user - if conf.exists(['disable']): - mdns['disabled'] = True - return mdns - - # Interface to repeat mDNS advertisements - if conf.exists(['interface']): - mdns['interfaces'] = conf.return_values(['interface']) - + conf = Config() + base = ['service', 'mdns', 'repeater'] + mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return mdns def verify(mdns): - if mdns is None: + if not mdns: return None - if mdns['disabled']: + if 'disable' in mdns: return None # We need at least two interfaces to repeat mDNS advertisments - if len(mdns['interfaces']) < 2: + if 'interface' not in mdns or len(mdns['interface']) < 2: raise ConfigError('mDNS repeater requires at least 2 configured interfaces!') # For mdns-repeater to work it is essential that the interfaces has # an IPv4 address assigned - for interface in mdns['interfaces']: - if AF_INET in ifaddresses(interface).keys(): - if len(ifaddresses(interface)[AF_INET]) < 1: - raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) + for interface in mdns['interface']: + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist!') + + if AF_INET not in ifaddresses(interface): + raise ConfigError('mDNS repeater requires an IPv4 address to be ' + f'configured on interface "{interface}"') return None def generate(mdns): - if mdns is None: + if not mdns: return None - if mdns['disabled']: + if 'disable' in mdns: print('Warning: mDNS repeater will be deactivated because it is disabled') return None @@ -87,7 +72,7 @@ def generate(mdns): return None def apply(mdns): - if (mdns is None) or mdns['disabled']: + if not mdns or 'disable' in mdns: call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 3149bbb2f..45d3806d5 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -85,8 +85,11 @@ default_config_data = { 'thread_cnt': get_half_cpus() } -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base_path = ['service', 'pppoe-server'] if not conf.exists(base_path): return None @@ -242,7 +245,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -253,6 +257,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) @@ -417,6 +424,9 @@ def verify(pppoe): if len(pppoe['dnsv6']) > 3: raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + if not pppoe['interfaces']: + raise ConfigError('At least one listen interface must be defined!') + # local ippool and gateway settings config checks if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']: if not pppoe['ppp_gw']: diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index ef6148ebd..687d7068f 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -16,145 +16,88 @@ import os -from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +from vyos.configdict import dict_merge from vyos.template import render - +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/run/radvd/radvd.conf' -default_config_data = { - 'interfaces': [] -} - -def get_config(): - rtradv = default_config_data - conf = Config() - base_level = ['service', 'router-advert'] - - if not conf.exists(base_level): - return rtradv - - for interface in conf.list_nodes(base_level + ['interface']): - intf = { - 'name': interface, - 'hop_limit' : '64', - 'default_lifetime': '', - 'default_preference': 'medium', - 'dnssl': [], - 'link_mtu': '', - 'managed_flag': 'off', - 'interval_max': '600', - 'interval_min': '', - 'name_server': [], - 'other_config_flag': 'off', - 'prefixes' : [], - 'reachable_time': '0', - 'retrans_timer': '0', - 'send_advert': 'on' - } - - # set config level first to reduce boilerplate code - conf.set_level(base_level + ['interface', interface]) - - if conf.exists(['hop-limit']): - intf['hop_limit'] = conf.return_value(['hop-limit']) - - if conf.exists(['default-lifetime']): - intf['default_lifetime'] = conf.return_value(['default-lifetime']) - - if conf.exists(['default-preference']): - intf['default_preference'] = conf.return_value(['default-preference']) - - if conf.exists(['dnssl']): - intf['dnssl'] = conf.return_values(['dnssl']) - - if conf.exists(['link-mtu']): - intf['link_mtu'] = conf.return_value(['link-mtu']) - - if conf.exists(['managed-flag']): - intf['managed_flag'] = 'on' - - if conf.exists(['interval', 'max']): - intf['interval_max'] = conf.return_value(['interval', 'max']) - - if conf.exists(['interval', 'min']): - intf['interval_min'] = conf.return_value(['interval', 'min']) - - if conf.exists(['name-server']): - intf['name_server'] = conf.return_values(['name-server']) +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'router-advert'] + rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_interface_values = defaults(base + ['interface']) + # we deal with prefix defaults later on + if 'prefix' in default_interface_values: + del default_interface_values['prefix'] + + default_prefix_values = defaults(base + ['interface', 'prefix']) + + if 'interface' in rtradv: + for interface in rtradv['interface']: + rtradv['interface'][interface] = dict_merge( + default_interface_values, rtradv['interface'][interface]) + + if 'prefix' in rtradv['interface'][interface]: + for prefix in rtradv['interface'][interface]['prefix']: + rtradv['interface'][interface]['prefix'][prefix] = dict_merge( + default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) + + if 'name_server' in rtradv['interface'][interface]: + # always use a list when dealing with nameservers - eases the template generation + if isinstance(rtradv['interface'][interface]['name_server'], str): + rtradv['interface'][interface]['name_server'] = [ + rtradv['interface'][interface]['name_server']] - if conf.exists(['other-config-flag']): - intf['other_config_flag'] = 'on' - - if conf.exists(['reachable-time']): - intf['reachable_time'] = conf.return_value(['reachable-time']) - - if conf.exists(['retrans-timer']): - intf['retrans_timer'] = conf.return_value(['retrans-timer']) - - if conf.exists(['no-send-advert']): - intf['send_advert'] = 'off' - - for prefix in conf.list_nodes(['prefix']): - tmp = { - 'prefix' : prefix, - 'autonomous_flag' : 'on', - 'on_link' : 'on', - 'preferred_lifetime': 14400, - 'valid_lifetime' : 2592000 - - } - - # set config level first to reduce boilerplate code - conf.set_level(base_level + ['interface', interface, 'prefix', prefix]) - - if conf.exists(['no-autonomous-flag']): - tmp['autonomous_flag'] = 'off' - - if conf.exists(['no-on-link-flag']): - tmp['on_link'] = 'off' - - if conf.exists(['preferred-lifetime']): - tmp['preferred_lifetime'] = int(conf.return_value(['preferred-lifetime'])) + return rtradv - if conf.exists(['valid-lifetime']): - tmp['valid_lifetime'] = int(conf.return_value(['valid-lifetime'])) +def verify(rtradv): + if not rtradv: + return None - intf['prefixes'].append(tmp) + if 'interface' not in rtradv: + return None - rtradv['interfaces'].append(intf) + for interface in rtradv['interface']: + interface = rtradv['interface'][interface] + if 'prefix' in interface: + for prefix in interface['prefix']: + prefix = interface['prefix'][prefix] + valid_lifetime = prefix['valid_lifetime'] + if valid_lifetime == 'infinity': + valid_lifetime = 4294967295 - return rtradv + preferred_lifetime = prefix['preferred_lifetime'] + if preferred_lifetime == 'infinity': + preferred_lifetime = 4294967295 -def verify(rtradv): - for interface in rtradv['interfaces']: - for prefix in interface['prefixes']: - if not (prefix['valid_lifetime'] > prefix['preferred_lifetime']): - raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if not (int(valid_lifetime) > int(preferred_lifetime)): + raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') return None def generate(rtradv): - if not rtradv['interfaces']: + if not rtradv: return None - render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True) - - # adjust file permissions of new configuration file - if os.path.exists(config_file): - os.chmod(config_file, S_IRUSR | S_IWUSR | S_IRGRP) - + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644) return None def apply(rtradv): - if not rtradv['interfaces']: + if not rtradv: # bail out early - looks like removal from running config call('systemctl stop radvd.service') if os.path.exists(config_file): @@ -163,6 +106,7 @@ def apply(rtradv): return None call('systemctl restart radvd.service') + return None if __name__ == '__main__': diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index ffb0b700d..a19fa72d8 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -28,11 +28,14 @@ from vyos.xml import defaults from vyos import airbag airbag.enable() -config_file = r'/etc/ssh/sshd_config' +config_file = r'/run/ssh/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['service', 'ssh'] if not conf.exists(base): return None @@ -42,6 +45,8 @@ def get_config(): # options which we need to update into the dictionary retrived. default_values = defaults(base) ssh = dict_merge(default_values, ssh) + # pass config file path - used in override template + ssh['config_file'] = config_file return ssh diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 85f1e3771..64c9e6d05 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -35,9 +35,12 @@ default_config_data = { def sysctl(name, value): call('sysctl -wq {}={}'.format(name, value)) -def get_config(): +def get_config(config=None): ip_opt = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() conf.set_level('system ip') if conf.exists(''): if conf.exists('arp table-size'): diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 3417c609d..f70ec2631 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -41,9 +41,12 @@ default_config_data = { def sysctl(name, value): call('sysctl -wq {}={}'.format(name, value)) -def get_config(): +def get_config(config=None): ip_opt = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() conf.set_level('system ipv6') if conf.exists(''): ip_opt['disable_addr_assignment'] = conf.exists('disable') diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index 5c0adc921..569010735 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -41,9 +41,12 @@ default_config_data = { 'motd': motd } -def get_config(): +def get_config(config=None): banner = default_config_data - conf = Config() + if config: + conf = config + else: + conf = Config() base_level = ['system', 'login', 'banner'] if not conf.exists(base_level): diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 93d4cc679..2aca199f9 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -56,9 +56,12 @@ def get_local_users(): return local_users -def get_config(): +def get_config(config=None): login = default_config_data - conf = Config() + if config: + conf = config + else: + conf = Config() base_level = ['system', 'login'] # We do not need to check if the nodes exist or not and bail out early @@ -72,7 +75,7 @@ def get_config(): user = { 'name': username, 'password_plaintext': '', - 'password_encred': '!', + 'password_encrypted': '!', 'public_keys': [], 'full_name': '', 'home_dir': '/home/' + username, diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py index d7c5c0443..6ac35a4ab 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-options.py @@ -22,23 +22,28 @@ from sys import exit from vyos.config import Config from vyos.template import render from vyos.util import call +from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/etc/curlrc' +curlrc_config = r'/etc/curlrc' +ssh_config = r'/etc/ssh/ssh_config' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['system', 'options'] options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return options def verify(options): - if 'http_client' in options.keys(): + if 'http_client' in options: config = options['http_client'] - if 'source_interface' in config.keys(): + if 'source_interface' in config: if not config['source_interface'] in interfaces(): raise ConfigError(f'Source interface {source_interface} does not ' f'exist'.format(**config)) @@ -46,10 +51,21 @@ def verify(options): if {'source_address', 'source_interface'} <= set(config): raise ConfigError('Can not define both HTTP source-interface and source-address') + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + + if 'ssh_client' in options: + config = options['ssh_client'] + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + return None def generate(options): - render(config_file, 'system/curlrc.tmpl', options, trim_blocks=True) + render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True) + render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True) return None def apply(options): @@ -63,12 +79,20 @@ def apply(options): if os.path.exists(systemd_action_file): os.unlink(systemd_action_file) - if 'ctrl_alt_del_action' in options.keys(): + if 'ctrl_alt_del_action' in options: if options['ctrl_alt_del_action'] == 'reboot': os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) elif options['ctrl_alt_del_action'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) + if 'http_client' not in options: + if os.path.exists(curlrc_config): + os.unlink(curlrc_config) + + if 'ssh_client' not in options: + if os.path.exists(ssh_config): + os.unlink(ssh_config) + # Reboot system on kernel panic with open('/proc/sys/kernel/panic', 'w') as f: if 'reboot_on_panic' in options.keys(): diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index cfc1ca55f..d29109c41 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -27,8 +27,11 @@ from vyos.template import render from vyos import airbag airbag.enable() -def get_config(): - c = Config() +def get_config(config=None): + if config: + c = config + else: + c = Config() if not c.exists('system syslog'): return None c.set_level('system syslog') diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py index 0f4513122..4d9f017a6 100755 --- a/src/conf_mode/system-timezone.py +++ b/src/conf_mode/system-timezone.py @@ -29,9 +29,12 @@ default_config_data = { 'name': 'UTC' } -def get_config(): +def get_config(config=None): tz = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() if conf.exists('system time-zone'): tz['name'] = conf.return_value('system time-zone') diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py index 30ea89098..874f93923 100755 --- a/src/conf_mode/system-wifi-regdom.py +++ b/src/conf_mode/system-wifi-regdom.py @@ -34,9 +34,12 @@ default_config_data = { 'deleted' : False } -def get_config(): +def get_config(config=None): regdom = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base = ['system', 'wifi-regulatory-domain'] # Check if interface has been removed diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 6f83335f3..b17818797 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -26,8 +26,11 @@ airbag.enable() by_bus_dir = '/dev/serial/by-bus' -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['system', 'console'] # retrieve configuration at once diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py new file mode 100755 index 000000000..a540d1b9e --- /dev/null +++ b/src/conf_mode/system_lcd.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos.util import find_device_file +from vyos.template import render +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +lcdd_conf = '/run/LCDd/LCDd.conf' +lcdproc_conf = '/run/lcdproc/lcdproc.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'lcd'] + lcd = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) + # Return (possibly empty) dictionary + return lcd + +def verify(lcd): + if not lcd: + return None + + if 'model' in lcd and lcd['model'] in ['sdec']: + # This is a fixed LCD display, no device needed - bail out early + return None + + if not {'device', 'model'} <= set(lcd): + raise ConfigError('Both device and driver must be set!') + + return None + +def generate(lcd): + if not lcd: + return None + + if 'device' in lcd: + lcd['device'] = find_device_file(lcd['device']) + + # Render config file for daemon LCDd + render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd, trim_blocks=True) + # Render config file for client lcdproc + render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True) + + return None + +def apply(lcd): + if not lcd: + call('systemctl stop lcdproc.service LCDd.service') + + for file in [lcdd_conf, lcdproc_conf]: + if os.path.exists(file): + os.remove(file) + else: + # Restart server + call('systemctl restart LCDd.service lcdproc.service') + + return None + +if __name__ == '__main__': + try: + config_dict = get_config() + verify(config_dict) + generate(config_dict) + apply(config_dict) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py index 51d8684cb..129be5d3c 100755 --- a/src/conf_mode/task_scheduler.py +++ b/src/conf_mode/task_scheduler.py @@ -53,8 +53,11 @@ def make_command(executable, arguments): else: return("sg vyattacfg \"{0}\"".format(executable)) -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() conf.set_level("system task-scheduler task") task_names = conf.list_nodes("") tasks = [] diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index d31851bef..ad5ee9c33 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -40,9 +40,12 @@ default_config_data = { 'listen': [] } -def get_config(): +def get_config(config=None): tftpd = deepcopy(default_config_data) - conf = Config() + if config: + conf = config + else: + conf = Config() base = ['service', 'tftp-server'] if not conf.exists(base): return None diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 88df2902e..13831dcd8 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -70,8 +70,11 @@ default_config_data = { 'thread_cnt': get_half_cpus() } -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base_path = ['vpn', 'l2tp', 'remote-access'] if not conf.exists(base_path): return None @@ -151,7 +154,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -162,6 +166,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py new file mode 100755 index 000000000..af8604972 --- /dev/null +++ b/src/conf_mode/vpn_openconnect.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.xml import defaults +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from crypt import crypt, mksalt, METHOD_SHA512 + +from vyos import airbag +airbag.enable() + +cfg_dir = '/run/ocserv' +ocserv_conf = cfg_dir + '/ocserv.conf' +ocserv_passwd = cfg_dir + '/ocpasswd' +radius_cfg = cfg_dir + '/radiusclient.conf' +radius_servers = cfg_dir + '/radius_servers' + + +# Generate hash from user cleartext password +def get_hash(password): + return crypt(password, mksalt(METHOD_SHA512)) + + +def get_config(): + conf = Config() + base = ['vpn', 'openconnect'] + if not conf.exists(base): + return None + + ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + default_values = defaults(base) + ocserv = dict_merge(default_values, ocserv) + return ocserv + + +def verify(ocserv): + if ocserv is None: + return None + + # Check authentication + if "authentication" in ocserv: + if "mode" in ocserv["authentication"]: + if "local" in ocserv["authentication"]["mode"]: + if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at leat one user') + else: + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + raise ConfigError(f'password required for user {user}') + else: + raise ConfigError('openconnect authentication mode required') + else: + raise ConfigError('openconnect authentication credentials required') + + # Check ssl + if "ssl" in ocserv: + req_cert = ['ca_cert_file', 'cert_file', 'key_file'] + for cert in req_cert: + if not cert in ocserv["ssl"]: + raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-'))) + else: + raise ConfigError('openconnect ssl required') + + # Check network settings + if "network_settings" in ocserv: + if "push_route" in ocserv["network_settings"]: + # Replace default route + if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: + ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") + ocserv["network_settings"]["push_route"].append("default") + else: + ocserv["network_settings"]["push_route"] = "default" + else: + raise ConfigError('openconnect network settings required') + + +def generate(ocserv): + if not ocserv: + return None + + if "radius" in ocserv["authentication"]["mode"]: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + else: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) + # Render local users + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + + # Render config + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) + + + +def apply(ocserv): + if not ocserv: + call('systemctl stop ocserv.service') + for file in [ocserv_conf, ocserv_passwd]: + if os.path.exists(file): + os.unlink(file) + else: + call('systemctl restart ocserv.service') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 4536692d2..9f3b40534 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -56,8 +56,11 @@ default_pptp = { 'thread_cnt': get_half_cpus() } -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base_path = ['vpn', 'pptp', 'remote-access'] if not conf.exists(base_path): return None @@ -111,7 +114,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -122,6 +126,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 4c4d8e403..7fc370f99 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -65,10 +65,13 @@ default_config_data = { 'thread_cnt' : get_half_cpus() } -def get_config(): +def get_config(config=None): sstp = deepcopy(default_config_data) base_path = ['vpn', 'sstp'] - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists(base_path): return None @@ -118,7 +121,8 @@ def get_config(): 'server' : server, 'key' : '', 'fail_time' : 0, - 'port' : '1812' + 'port' : '1812', + 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) @@ -129,6 +133,9 @@ def get_config(): if conf.exists(['port']): radius['port'] = conf.return_value(['port']) + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + if conf.exists(['key']): radius['key'] = conf.return_value(['key']) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 56ca813ff..2f4da0240 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -76,8 +76,11 @@ def vrf_routing(c, match): return matched -def get_config(): - conf = Config() +def get_config(config=None): + if config: + conf = config + else: + conf = Config() vrf_config = deepcopy(default_config_data) cfg_base = ['vrf'] diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 292eb0c78..f1ceb261b 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -32,11 +32,14 @@ from vyos.ifconfig.vrrp import VRRP from vyos import airbag airbag.enable() -def get_config(): +def get_config(config=None): vrrp_groups = [] sync_groups = [] - config = vyos.config.Config() + if config: + config = config + else: + config = vyos.config.Config() # Get the VRRP groups for group_name in config.list_nodes("high-availability vrrp group"): diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py index fb4644d5a..dc7c64684 100755 --- a/src/conf_mode/vyos_cert.py +++ b/src/conf_mode/vyos_cert.py @@ -103,10 +103,13 @@ def generate_self_signed(cert_data): if san_config: san_config.close() -def get_config(): +def get_config(config=None): vyos_cert = vyos.defaults.vyos_cert_data - conf = Config() + if config: + conf = config + else: + conf = Config() if not conf.exists('service https certificates system-generated-certificate'): return None else: diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index f1167fcd2..d1161e704 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -40,7 +40,14 @@ function iptovtysh () { elif [ "$7" == "dev" ]; then VTYSH_DEV=$8 fi - VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE" + + # Add route to VRF routing table + local VTYSH_VRF_NAME=$(basename /sys/class/net/$VTYSH_DEV/upper_* | sed -e 's/upper_//') + if [ -n $VTYSH_VRF_NAME ]; then + VTYSH_VRF="vrf $VTYSH_VRF_NAME" + fi + VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE $VTYSH_VRF" + # delete route if the command is "del" if [ "$3" == "del" ] ; then VTYSH_CMD="no $VTYSH_CMD" diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index 01981ad04..b768e1ae5 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -15,8 +15,14 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then # try to delete default ip route for router in $old_routers; do - logmsg info "Deleting default route: via $router dev ${interface}" - ip -4 route del default via $router dev ${interface} + # check if we are bound to a VRF + local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//') + if [ -n $vrf_name ]; then + vrf="vrf $vrf_name" + fi + + logmsg info "Deleting default route: via $router dev ${interface} ${vrf}" + ip -4 route del default via $router dev ${interface} ${vrf} done # delete rfc3442 routes diff --git a/src/etc/systemd/system/LCDd.service.d/override.conf b/src/etc/systemd/system/LCDd.service.d/override.conf new file mode 100644 index 000000000..5f3f0dc95 --- /dev/null +++ b/src/etc/systemd/system/LCDd.service.d/override.conf @@ -0,0 +1,8 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/LCDd -c /run/LCDd/LCDd.conf + diff --git a/src/etc/systemd/system/ocserv.service.d/override.conf b/src/etc/systemd/system/ocserv.service.d/override.conf new file mode 100644 index 000000000..89dbb153f --- /dev/null +++ b/src/etc/systemd/system/ocserv.service.d/override.conf @@ -0,0 +1,14 @@ +[Unit] +RequiresMountsFor=/run +ConditionPathExists=/run/ocserv/ocserv.conf +After= +After=vyos-router.service +After=dbus.service + +[Service] +WorkingDirectory=/run/ocserv +PIDFile= +PIDFile=/run/ocserv/ocserv.pid +ExecStart= +ExecStart=/usr/sbin/ocserv --foreground --pid-file /run/ocserv/ocserv.pid --config /run/ocserv/ocserv.conf + diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf index 44c4345e1..c2f640cf5 100644 --- a/src/etc/systemd/system/radvd.service.d/override.conf +++ b/src/etc/systemd/system/radvd.service.d/override.conf @@ -1,17 +1,17 @@ -[Unit]
-ConditionPathExists=/run/radvd/radvd.conf
-After=
-After=vyos-router.service
-
-[Service]
-WorkingDirectory=
-WorkingDirectory=/run/radvd
-ExecStartPre=
-ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
-ExecStart=
-ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid
-ExecReload=
-ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
-ExecReload=/bin/kill -HUP $MAINPID
-PIDFile=
-PIDFile=/run/radvd/radvd.pid
+[Unit] +ConditionPathExists=/run/radvd/radvd.conf +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/radvd +ExecStartPre= +ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecStart= +ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid +ExecReload= +ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecReload=/bin/kill -HUP $MAINPID +PIDFile= +PIDFile=/run/radvd/radvd.pid diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index a9fa15778..c2da1bb11 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -27,12 +27,12 @@ import sys import tempfile import vyos.defaults import vyos.remote -from vyos.config import Config, VyOSError +from vyos.configsource import ConfigSourceSession, VyOSError from vyos.migrator import Migrator, VirtualMigrator, MigratorError -class LoadConfig(Config): +class LoadConfig(ConfigSourceSession): """A subclass for calling 'loadFile'. - This does not belong in config.py, and only has a single caller. + This does not belong in configsource.py, and only has a single caller. """ def load_config(self, path): return self._run(['/bin/cli-shell-api','loadFile',path]) diff --git a/src/migration-scripts/interfaces/11-to-12 b/src/migration-scripts/interfaces/11-to-12 new file mode 100755 index 000000000..0dad24642 --- /dev/null +++ b/src/migration-scripts/interfaces/11-to-12 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - rename 'dhcpv6-options prefix-delegation' from single node to a new tag node +# 'dhcpv6-options pd 0' +# - delete 'sla-len' from CLI - value is calculated on demand + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + # cache current config tree + base_path = ['interfaces', type, interface, 'dhcpv6-options'] + old_base = base_path + ['prefix-delegation'] + new_base = base_path + ['pd'] + if config.exists(old_base): + config.set(new_base) + config.set_tag(new_base) + config.copy(old_base, new_base + ['0']) + config.delete(old_base) + + for pd in config.list_nodes(new_base): + for tmp in config.list_nodes(new_base + [pd, 'interface']): + sla_config = new_base + [pd, 'interface', tmp, 'sla-len'] + if config.exists(sla_config): + config.delete(sla_config) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 index 8830ffdc7..a4051301f 100755 --- a/src/migration-scripts/interfaces/7-to-8 +++ b/src/migration-scripts/interfaces/7-to-8 @@ -17,8 +17,23 @@ # Split WireGuard endpoint into address / port nodes to make use of common # validators +import os + from sys import exit, argv from vyos.configtree import ConfigTree +from vyos.util import chown, chmod_750 + +def migrate_default_keys(): + kdir = r'/config/auth/wireguard' + if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): + location = f'{kdir}/default' + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) + os.rename(f'{kdir}/private.key', f'{location}/private.key') + os.rename(f'{kdir}/public.key', f'{location}/public.key') if __name__ == '__main__': if (len(argv) < 1): @@ -32,6 +47,8 @@ if __name__ == '__main__': config = ConfigTree(config_file) base = ['interfaces', 'wireguard'] + migrate_default_keys() + if not config.exists(base): # Nothing to do exit(0) diff --git a/src/migration-scripts/pppoe-server/2-to-3 b/src/migration-scripts/pppoe-server/2-to-3 index fa6ef02da..5f9730a41 100755 --- a/src/migration-scripts/pppoe-server/2-to-3 +++ b/src/migration-scripts/pppoe-server/2-to-3 @@ -17,7 +17,6 @@ # - remove primary/secondary identifier from nameserver import os -import sys from sys import argv, exit from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/pppoe-server/3-to-4 b/src/migration-scripts/pppoe-server/3-to-4 new file mode 100755 index 000000000..ed5a01625 --- /dev/null +++ b/src/migration-scripts/pppoe-server/3-to-4 @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# change mppe node to a leaf node with value prefer + +import os + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + mppe_base = base + ['ppp-options', 'mppe'] + if config.exists(mppe_base): + # drop node first ... + config.delete(mppe_base) + # ... and set new default + config.set(mppe_base, value='prefer') + + print(config.to_string()) + exit(1) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 9d0417cd4..6586cbceb 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -29,47 +29,42 @@ from vyos.logger import syslog uacctd_pidfile = '/var/run/uacctd.pid' uacctd_pipefile = '/tmp/uacctd.pipe' - -# check if ports argument have correct format -def _is_ports(ports): - # define regex for checking - regex_filter = re.compile(r'^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$|^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])-(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$|^((\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]),)+(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if not regex_filter.search(ports): - raise argparse.ArgumentTypeError("Invalid ports: {}".format(ports)) - - # check which type nitation is used: single port, ports list, ports range - # single port - regex_filter = re.compile(r'^(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if regex_filter.search(ports): - filter_ports = {'type': 'single', 'value': int(ports)} - - # ports list - regex_filter = re.compile(r'^((\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]),)+(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])') - if regex_filter.search(ports): - filter_ports = {'type': 'list', 'value': list(map(int, ports.split(',')))} - - # ports range - regex_filter = re.compile(r'^(?P<first>\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])-(?P<second>\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$') - if regex_filter.search(ports): - # check if second number is greater than the first - if int(regex_filter.search(ports).group('first')) >= int(regex_filter.search(ports).group('second')): - raise argparse.ArgumentTypeError("Invalid ports: {}".format(ports)) - filter_ports = {'type': 'range', 'value': range(int(regex_filter.search(ports).group('first')), int(regex_filter.search(ports).group('second')))} - - # if all above failed - if not filter_ports: - raise argparse.ArgumentTypeError("Failed to parse: {}".format(ports)) +def parse_port(port): + try: + port_num = int(port) + if (port_num >= 0) and (port_num <= 65535): + return port_num + else: + raise ValueError("out of the 0-65535 range".format(port)) + except ValueError as e: + raise ValueError("Incorrect port number \'{0}\': {1}".format(port, e)) + +def parse_ports(arg): + if re.match(r'^\d+$', arg): + # Single port + port = parse_port(arg) + return {"type": "single", "value": port} + elif re.match(r'^\d+\-\d+$', arg): + # Port range + ports = arg.split("-") + ports = list(map(parse_port, ports)) + if ports[0] > ports[1]: + raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg)) + else: + return {"type": "range", "value": (ports[0], ports[1])} + elif re.match(r'^\d+,.*\d$', arg): + # Port list + ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4' + ports = list(map(parse_port, ports)) + return {"type": "list", "value": ports} else: - return filter_ports - + raise ValueError("Malformed port spec \'{0}\'".format(arg)) # check if host argument have correct format -def _is_host(host): +def check_host(host): # define regex for checking if not ipaddress.ip_address(host): - raise argparse.ArgumentTypeError("Invalid host: {}".format(host)) - return host - + raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host)) # check if flow-accounting running def _uacctd_running(): @@ -205,12 +200,21 @@ cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting') cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon') cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display') cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration') -cmd_args_parser.add_argument('--host', type=_is_host, required=False, help='host address for output filtration') -cmd_args_parser.add_argument('--ports', type=_is_ports, required=False, help='ports number for output filtration') -cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtration') +cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering') +cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering') +cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering') # parse arguments cmd_args = cmd_args_parser.parse_args() +try: + if cmd_args.host: + check_host(cmd_args.host) + + if cmd_args.ports: + cmd_args.ports = parse_ports(cmd_args.ports) +except ValueError as e: + print(e) + sys.exit(1) # main logic # do nothing if uacctd daemon is not running diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index 5d48e3210..06958c605 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -14,19 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - import argparse import jinja2 +import json -from xml.dom import minidom from sys import exit from tabulate import tabulate -from vyos.util import popen +from vyos.util import cmd from vyos.config import Config parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") +parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") # Please be careful if you edit the template. @@ -35,108 +35,61 @@ lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S Device ID Local Proto Cap Platform Port ID --------- ----- ----- --- -------- ------- -{% for n in neighbors -%} -{{ "%-25s" | format(n.chassis) }} {{ "%-9s" | format(n.interface) }} {{ "%-6s" | format(n.proto) }} {{ "%-5s" | format(n.cap) }} {{ "%-20s" | format(n.platform) }} {{ n.port }} -{% endfor -%} +{% for neighbor in neighbors %} +{% for local_if, info in neighbor.items() %} +{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }} +{% endfor %} +{% endfor %} """ -def _get_neighbors(): - command = '/usr/sbin/lldpcli -f xml show neighbors' - out,_ = popen(command) - return out - -def extract_neighbor(neighbor): - """ - Extract LLDP neighbor information from XML document passed as param neighbor - - <lldp> - <interface label="Interface" name="eth0" via="LLDP" rid="3" age="0 day, 00:17:42"> - <chassis label="Chassis"> - <id label="ChassisID" type="mac">00:50:56:9d:a6:11</id> - <name label="SysName">VyOS</name> - <descr label="SysDescr">VyOS unknown</descr> - <mgmt-ip label="MgmtIP">172.18.254.203</mgmt-ip> - <mgmt-ip label="MgmtIP">fe80::250:56ff:fe9d:a611</mgmt-ip> - <capability label="Capability" type="Bridge" enabled="off"/> - <capability label="Capability" type="Router" enabled="on"/> - <capability label="Capability" type="Wlan" enabled="off"/> - <capability label="Capability" type="Station" enabled="off"/> - </chassis> - <port label="Port"> - <id label="PortID" type="mac">00:50:56:9d:a6:11</id> - <descr label="PortDescr">eth0</descr> - <ttl label="TTL">120</ttl> - <auto-negotiation label="PMD autoneg" supported="no" enabled="no"> - <current label="MAU oper type">10GigBaseCX4 - X copper over 8 pair 100-Ohm balanced cable</current> - </auto-negotiation> - </port> - <vlan label="VLAN" vlan-id="203">eth0.203</vlan> - <lldp-med label="LLDP-MED"> - <device-type label="Device Type">Network Connectivity Device</device-type> - <capability label="Capability" type="Capabilities" available="yes"/> - <capability label="Capability" type="Policy" available="yes"/> - <capability label="Capability" type="Location" available="yes"/> - <capability label="Capability" type="MDI/PSE" available="yes"/> - <capability label="Capability" type="MDI/PD" available="yes"/> - <capability label="Capability" type="Inventory" available="yes"/> - <inventory label="Inventory"> - <hardware label="Hardware Revision">None</hardware> - <software label="Software Revision">4.19.54-amd64-vyos</software> - <firmware label="Firmware Revision">6.00</firmware> - <serial label="Serial Number">VMware-42 1d cf 87 ab 7f da 7e-3</serial> - <manufacturer label="Manufacturer">VMware, Inc.</manufacturer> - <model label="Model">VMware Virtual Platform</model> - <asset label="Asset ID">No Asset Tag</asset> - </inventory> - </lldp-med> - </interface> - </lldp> - """ - - device = { - 'interface' : neighbor.getAttribute('name'), - 'chassis' : '', - 'proto' : neighbor.getAttribute('via'), - 'descr' : '', - 'cap' : '', - 'platform' : '', - 'port' : '' - } - - # first change to <chassis> node and then retrieve <name> and <descr> - chassis = neighbor.getElementsByTagName('chassis') - device['chassis'] = chassis[0].getElementsByTagName('name')[0].firstChild.data - # Cisco IOS comes with a ',' remove character .... - device['platform'] = chassis[0].getElementsByTagName('descr')[0].firstChild.data[:20].replace(',',' ') - - # extract capabilities - for capability in chassis[0].getElementsByTagName('capability'): - # we are only interested in enabled capabilities ... - if capability.getAttribute('enabled') == "on": - if capability.getAttribute('type') == "Router": - device['cap'] += 'R' - elif capability.getAttribute('type') == "Bridge": - device['cap'] += 'B' - elif capability.getAttribute('type') == "Wlan": - device['cap'] += 'W' - elif capability.getAttribute('type') == "Station": - device['cap'] += 'S' - elif capability.getAttribute('type') == "Repeater": - device['cap'] += 'r' - elif capability.getAttribute('type') == "Telephone": - device['cap'] += 'T' - elif capability.getAttribute('type') == "Docsis": - device['cap'] += 'D' - elif capability.getAttribute('type') == "Other": - device['cap'] += 'O' - - # first change to <port> node and then retrieve <descr> - port = neighbor.getElementsByTagName('port') - port = port[0].getElementsByTagName('descr')[0].firstChild.data - device['port'] = port - - - return device +def get_neighbors(): + return cmd('/usr/sbin/lldpcli -f json show neighbors') + +def parse_data(data): + output = [] + for tmp in data: + for local_if, values in tmp.items(): + for chassis, c_value in values.get('chassis', {}).items(): + capabilities = c_value['capability'] + if isinstance(capabilities, dict): + capabilities = [capabilities] + + cap = '' + for capability in capabilities: + if capability['enabled']: + if capability['type'] == 'Router': + cap += 'R' + if capability['type'] == 'Bridge': + cap += 'B' + if capability['type'] == 'Wlan': + cap += 'W' + if capability['type'] == 'Station': + cap += 'S' + if capability['type'] == 'Repeater': + cap += 'r' + if capability['type'] == 'Telephone': + cap += 'T' + if capability['type'] == 'Docsis': + cap += 'D' + if capability['type'] == 'Other': + cap += 'O' + + + remote_if = 'Unknown' + if 'descr' in values.get('port', {}): + remote_if = values.get('port', {}).get('descr') + elif 'id' in values.get('port', {}): + remote_if = values.get('port', {}).get('id').get('value', 'Unknown') + + output.append({local_if: {'chassis': chassis, + 'remote_if': remote_if, + 'proto': values.get('via','Unknown'), + 'platform': c_value.get('descr', 'Unknown'), + 'capabilities': cap}}) + + + output = {'neighbors': output} + return output if __name__ == '__main__': args = parser.parse_args() @@ -147,24 +100,26 @@ if __name__ == '__main__': print('Service LLDP is not configured') exit(0) - if args.all: - neighbors = minidom.parseString(_get_neighbors()) - for neighbor in neighbors.getElementsByTagName('interface'): - tmp['neighbors'].append( extract_neighbor(neighbor) ) + if args.detail: + print(cmd('/usr/sbin/lldpctl -f plain')) + exit(0) + elif args.all or args.interface: + tmp = json.loads(get_neighbors()) - elif args.interface: - neighbors = minidom.parseString(_get_neighbors()) - for neighbor in neighbors.getElementsByTagName('interface'): - # check if neighbor appeared on proper interface - if neighbor.getAttribute('name') == args.interface: - tmp['neighbors'].append( extract_neighbor(neighbor) ) + if args.all: + neighbors = tmp['lldp']['interface'] + elif args.interface: + neighbors = [] + for neighbor in tmp['lldp']['interface']: + if args.interface in neighbor: + neighbors.append(neighbor) else: parser.print_help() exit(1) - tmpl = jinja2.Template(lldp_out) - config_text = tmpl.render(tmp) + tmpl = jinja2.Template(lldp_out, trim_blocks=True) + config_text = tmpl.render(parse_data(neighbors)) print(config_text) exit(0) diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py new file mode 100755 index 000000000..ef9fe618c --- /dev/null +++ b/src/op_mode/openconnect-control.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import argparse +import json + +from vyos.config import Config +from vyos.util import popen, run, DEVNULL +from tabulate import tabulate + +occtl = '/usr/bin/occtl' +occtl_socket = '/run/ocserv/occtl.socket' + +def show_sessions(): + out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL) + if code: + sys.exit('Cannot get openconnect users information') + else: + headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"] + sessions = json.loads(out) + ses_list = [] + for ses in sessions: + ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]]) + if len(ses_list) > 0: + print(tabulate(ses_list, headers)) + else: + print("No active openconnect sessions") + +def is_ocserv_configured(): + if not Config().exists_effective('vpn openconnect'): + print("vpn openconnect server is not configured") + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) + parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) + args = parser.parse_args() + + + # Check is Openconnect server configured + is_ocserv_configured() + + if args.action == "restart": + run("systemctl restart ocserv") + sys.exit(0) + elif args.action == "show_sessions": + show_sessions() + +if __name__ == '__main__': + main() diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index e56952c38..29b430d53 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -118,7 +118,8 @@ options = { 'vrf': { 'ping': 'sudo ip vrf exec {value} {command}', 'type': '<vrf>', - 'help': 'Use specified VRF table' + 'help': 'Use specified VRF table', + 'dflt': 'default', }, 'verbose': { 'ping': '{command} -v', @@ -207,6 +208,11 @@ if __name__ == '__main__': sys.stdout.write(options[matched[0]]['type']) sys.exit(0) + for name,option in options.items(): + if 'dflt' in option and name not in args: + args.append(name) + args.append(option['dflt']) + try: ip = socket.gethostbyname(host) except socket.gaierror: diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index f9577e57e..ff1e3cc56 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -161,7 +161,8 @@ def get_pool_size(config, pool): start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - size += int(ip_address(stop)) - int(ip_address(start)) + # Add +1 because both range boundaries are inclusive + size += int(ip_address(stop)) - int(ip_address(start)) + 1 return size diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 46571c0c0..d4dae3cd1 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -220,8 +220,7 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp): oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ] admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] - # do not ask me why 56, it was the number in the perl code ... - descs = list(split_text(interface.get_alias(),56)) + descs = list(split_text(interface.get_alias(),0)) while intf or oper or admin or addrs or descs: i = intf.pop(0) if intf else '' diff --git a/src/op_mode/show_system_integrity.py b/src/op_mode/show_system_integrity.py new file mode 100755 index 000000000..c34d41e80 --- /dev/null +++ b/src/op_mode/show_system_integrity.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import json +from datetime import datetime, timedelta + +version_file = r'/usr/share/vyos/version.json' + + +def _get_sys_build_version(): + if not os.path.exists(version_file): + return None + buf = open(version_file, 'r').read() + j = json.loads(buf) + if not 'built_on' in j: + return None + return datetime.strptime(j['built_on'], '%a %d %b %Y %H:%M %Z') + + +def _check_pkgs(build_stamp): + pkg_diffs = { + 'buildtime': str(build_stamp), + 'pkg': {} + } + + pkg_info = os.listdir('/var/lib/dpkg/info/') + for file in pkg_info: + if re.search('\.list$', file): + fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime + dt_str = (datetime.utcfromtimestamp( + fts).strftime('%Y-%m-%d %H:%M:%S')) + fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') + if fdt > build_stamp: + pkg_diffs['pkg'].update( + {str(re.sub('\.list', '', file)): str(fdt)}) + + if len(pkg_diffs['pkg']) != 0: + return pkg_diffs + else: + return None + + +if __name__ == '__main__': + built_date = _get_sys_build_version() + if not built_date: + sys.exit(1) + pkgs = _check_pkgs(built_date) + if pkgs: + print ( + "The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime']) + for k, v in pkgs['pkg'].items(): + print ("installed: " + v + '\t' + k) diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py index d0d5c6785..5bbc2e1f1 100755 --- a/src/op_mode/show_version.py +++ b/src/op_mode/show_version.py @@ -27,7 +27,6 @@ from sys import exit from vyos.util import call parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions") parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") @@ -65,9 +64,5 @@ if __name__ == '__main__': tmpl = Template(version_output_tmpl) print(tmpl.render(version_data)) - if args.all: - print("Package versions:") - call("dpkg -l") - if args.funny: print(vyos.limericks.get_random()) diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py deleted file mode 100755 index c0e3d1095..000000000 --- a/src/op_mode/system_integrity.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# - -import sys -import os -import re -import itertools -from datetime import datetime, timedelta - -from vyos.util import cmd - -verf = r'/usr/libexec/vyos/op_mode/version.py' - -def get_sys_build_version(): - if not os.path.exists(verf): - return None - - a = cmd('/usr/libexec/vyos/op_mode/version.py') - if re.search('^Built on:.+',a, re.M) == None: - return None - - dt = ( re.sub('Built on: +','', re.search('^Built on:.+',a, re.M).group(0)) ) - return datetime.strptime(dt,'%a %d %b %Y %H:%M %Z') - -def check_pkgs(dt): - pkg_diffs = { - 'buildtime' : str(dt), - 'pkg' : {} - } - - pkg_info = os.listdir('/var/lib/dpkg/info/') - for file in pkg_info: - if re.search('\.list$', file): - fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime - dt_str = (datetime.utcfromtimestamp(fts).strftime('%Y-%m-%d %H:%M:%S')) - fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') - if fdt > dt: - pkg_diffs['pkg'].update( { str(re.sub('\.list','',file)) : str(fdt)}) - - if len(pkg_diffs['pkg']) != 0: - return pkg_diffs - else: - return None - -def main(): - dt = get_sys_build_version() - pkgs = check_pkgs(dt) - if pkgs != None: - print ("The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime']) - for k, v in pkgs['pkg'].items(): - print ("installed: " + v + '\t' + k) - -if __name__ == '__main__': - sys.exit( main() ) - diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 15bf63e81..e08bc983a 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -21,22 +21,17 @@ import shutil import syslog as sl import re +from vyos.config import Config from vyos.ifconfig import WireGuardIf - +from vyos.util import cmd +from vyos.util import run +from vyos.util import check_kmod from vyos import ConfigError -from vyos.config import Config -from vyos.util import cmd, run dir = r'/config/auth/wireguard' psk = dir + '/preshared.key' -def check_kmod(): - """ check if kmod is loaded, if not load it """ - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if run('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") +k_mod = 'wireguard' def generate_keypair(pk, pub): """ generates a keypair which is stored in /config/auth/wireguard """ @@ -106,7 +101,7 @@ def del_key_dir(kname): if __name__ == '__main__': - check_kmod() + check_kmod(k_mod) parser = argparse.ArgumentParser(description='wireguard key management') parser.add_argument( '--genkey', action="store_true", help='generate key-pair') diff --git a/src/services/vyos-configd b/src/services/vyos-configd new file mode 100755 index 000000000..75f84d3df --- /dev/null +++ b/src/services/vyos-configd @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +import grp +import re +import json +import logging +import signal +import importlib.util +import zmq + +from vyos.defaults import directories +from vyos.configsource import ConfigSourceString +from vyos.config import Config +from vyos import ConfigError + +CFG_GROUP = 'vyattacfg' + +debug = True + +logger = logging.getLogger(__name__) +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) + +if debug: + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) + +SOCKET_PATH = "ipc:///run/vyos-configd.sock" + +# Response error codes +R_SUCCESS = 1 +R_ERROR_COMMIT = 2 +R_ERROR_DAEMON = 4 +R_PASS = 8 + +vyos_conf_scripts_dir = directories['conf_mode'] +configd_include_file = os.path.join(directories['data'], 'configd-include.json') +configd_env_set_file = os.path.join(directories['data'], 'vyos-configd-env-set') +configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-unset') +# sourced on entering config session +configd_env_file = '/etc/default/vyos-configd-env' + +active_string = '' +session_string = '' + +def key_name_from_file_name(f): + return os.path.splitext(f)[0] + +def module_name_from_key(k): + return k.replace('-', '_') + +def path_from_file_name(f): + return os.path.join(vyos_conf_scripts_dir, f) + +# opt-in to be run by daemon +with open(configd_include_file) as f: + try: + include = json.load(f) + except OSError as e: + logger.critical(f"configd include file error: {e}") + sys.exit(1) + except json.JSONDecodeError as e: + logger.critical(f"JSON load error: {e}") + sys.exit(1) + +# import conf_mode scripts +(_, _, filenames) = next(iter(os.walk(vyos_conf_scripts_dir))) +filenames.sort() + +load_filenames = [f for f in filenames if f in include] +imports = [key_name_from_file_name(f) for f in load_filenames] +module_names = [module_name_from_key(k) for k in imports] +paths = [path_from_file_name(f) for f in load_filenames] +to_load = list(zip(module_names, paths)) + +modules = [] + +for x in to_load: + spec = importlib.util.spec_from_file_location(x[0], x[1]) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + modules.append(module) + +conf_mode_scripts = dict(zip(imports, modules)) + +exclude_set = {key_name_from_file_name(f) for f in filenames if f not in include} +include_set = {key_name_from_file_name(f) for f in filenames if f in include} + + +def run_script(script, config) -> int: + config.set_level([]) + try: + c = script.get_config(config) + script.verify(c) + script.generate(c) + script.apply(c) + except ConfigError as e: + logger.critical(e) + return R_ERROR_COMMIT + except Exception: + return R_ERROR_DAEMON + + return R_SUCCESS + +def initialization(socket): + # Reset config strings: + active_string = '' + session_string = '' + # zmq synchronous for ipc from single client: + active_string = socket.recv().decode() + resp = "active" + socket.send(resp.encode()) + session_string = socket.recv().decode() + resp = "session" + socket.send(resp.encode()) + + configsource = ConfigSourceString(running_config_text=active_string, + session_config_text=session_string) + + config = Config(config_source=configsource) + + return config + +def process_node_data(config, data) -> int: + if not config: + logger.critical(f"Empty config") + return R_ERROR_DAEMON + + script_name = None + + res = re.match(r'^.+\/([^/].+).py(VYOS_TAGNODE_VALUE=.+)?', data) + if res.group(1): + script_name = res.group(1) + if res.group(2): + env = res.group(2).split('=') + os.environ[env[0]] = env[1] + + if not script_name: + logger.critical(f"Missing script_name") + return R_ERROR_DAEMON + + if script_name in exclude_set: + return R_PASS + + result = run_script(conf_mode_scripts[script_name], config) + + return result + +def remove_if_file(f: str): + try: + os.remove(f) + except FileNotFoundError: + pass + except OSError: + raise + +def shutdown(): + remove_if_file(configd_env_file) + os.symlink(configd_env_unset_file, configd_env_file) + sys.exit(0) + +if __name__ == '__main__': + context = zmq.Context() + socket = context.socket(zmq.REP) + + # Set the right permissions on the socket, then change it back + o_mask = os.umask(0) + socket.bind(SOCKET_PATH) + os.umask(o_mask) + + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + os.environ['SUDO_USER'] = 'vyos' + os.environ['SUDO_GID'] = str(cfg_group.gr_gid) + + def sig_handler(signum, frame): + shutdown() + + signal.signal(signal.SIGTERM, sig_handler) + signal.signal(signal.SIGINT, sig_handler) + + # Define the vyshim environment variable + remove_if_file(configd_env_file) + os.symlink(configd_env_set_file, configd_env_file) + + config = None + + while True: + # Wait for next request from client + msg = socket.recv().decode() + logger.debug(f"Received message: {msg}") + message = json.loads(msg) + + if message["type"] == "init": + resp = "init" + socket.send(resp.encode()) + config = initialization(socket) + elif message["type"] == "node": + res = process_node_data(config, message["data"]) + response = res.to_bytes(1, byteorder=sys.byteorder) + logger.debug(f"Sending response {res}") + socket.send(response) + else: + logger.critical(f"Unexpected message: {message}") diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4c41fa96d..d5730d86c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,7 +32,6 @@ from waitress import serve from functools import wraps from vyos.configsession import ConfigSession, ConfigSessionError -from vyos.config import VyOSError DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' @@ -231,8 +230,6 @@ def retrieve_op(command): return error(400, "\"{0}\" is not a valid config format".format(config_format)) else: return error(400, "\"{0}\" is not a valid operation".format(op)) - except VyOSError as e: - return error(400, str(e)) except ConfigSessionError as e: return error(400, str(e)) except Exception as e: diff --git a/src/shim/Makefile b/src/shim/Makefile new file mode 100644 index 000000000..c8487e3c2 --- /dev/null +++ b/src/shim/Makefile @@ -0,0 +1,20 @@ +DEBUG = 0 + +CC := gcc +CFLAGS := -I./mkjson -L./mkjson/lib -DDEBUG=${DEBUG} +LIBS := -lmkjson -lzmq + +.PHONY: vyshim +vyshim: vyshim.c libmkjson + $(CC) $(CFLAGS) -o $@ $< $(LIBS) + +.PHONY: libmkjson +libmkjson: + $(MAKE) -C mkjson + +all: vyshim + +.PHONY: clean +clean: + $(MAKE) -C mkjson clean + rm -f vyshim diff --git a/src/shim/mkjson/LICENSE b/src/shim/mkjson/LICENSE new file mode 100644 index 000000000..8c4284c91 --- /dev/null +++ b/src/shim/mkjson/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jacek Wieczorek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/shim/mkjson/makefile b/src/shim/mkjson/makefile new file mode 100644 index 000000000..ba75399d2 --- /dev/null +++ b/src/shim/mkjson/makefile @@ -0,0 +1,30 @@ +CFLAGS = -Wall -Os -I. +CC = gcc +AR = ar + +#USE_ASPRINTF make flag can be used in order to encourage asprintf use inside the library +ifeq ($(USE_ASPRINTF),1) +CFLAGS += -D_GNU_SOURCE +endif + +#Builds object and a static library file +all: clean force + $(CC) $(CFLAGS) -c mkjson.c -o obj/mkjson.o + $(AR) -cvq lib/libmkjson.a obj/mkjson.o + $(AR) -t lib/libmkjson.a + +#Normal cleanup +clean: + -rm -rf obj + -rm -rf lib + +#Environment init +force: + -mkdir obj + -mkdir lib + +#Build the example snippet +example: all + gcc -o example examples/example.c -I. -Llib -lmkjson + + diff --git a/src/shim/mkjson/mkjson.c b/src/shim/mkjson/mkjson.c new file mode 100644 index 000000000..1172664fb --- /dev/null +++ b/src/shim/mkjson/mkjson.c @@ -0,0 +1,307 @@ +/* mkjson.c - a part of mkjson library + * + * Copyright (C) 2018 Jacek Wieczorek + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#include <mkjson.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> + +// Works like asprintf, but it's always there +// I don't want the name to collide with anything +static int allsprintf( char **strp, const char *fmt, ... ) +{ + int len; + va_list ap; + va_start( ap, fmt ); + + #ifdef _GNU_SOURCE + // Just hand everything to vasprintf, if it's available + len = vasprintf( strp, fmt, ap ); + #else + // Or do it the manual way + char *buf; + len = vsnprintf( NULL, 0, fmt, ap ); + if ( len >= 0 ) + { + buf = malloc( ++len ); + if ( buf != NULL ) + { + // Hopefully, that's the right way to do it + va_end( ap ); + va_start( ap, fmt ); + + // Write and return the data + len = vsnprintf( buf, len, fmt, ap ); + if ( len >= 0 ) + { + *strp = buf; + } + else + { + free( buf ); + } + } + } + #endif + + va_end( ap ); + return len; +} + +// Return JSON string built from va_arg arguments +// If no longer needed, should be passed to free() by user +char *mkjson( enum mkjson_container_type otype, int count, ... ) +{ + int i, len, goodchunks = 0, failure = 0; + char *json, *prefix, **chunks, ign; + + // Value - type and data + enum mkjson_value_type vtype; + const char *key; + long long int intval; + long double dblval; + const char *strval; + + // Since v0.9 count cannot be a negative value and datatype is indicated by a separate argument + // Since I'm not sure whether it's right to put assertions in libraries, the next line is commented out + // assert( count >= 0 && "After v0.9 negative count is prohibited; please use otype argument instead" ); + if ( count < 0 || ( otype != MKJSON_OBJ && otype != MKJSON_ARR ) ) return NULL; + + // Allocate chunk pointer array - on standard platforms each one should be NULL + chunks = calloc( count, sizeof( char* ) ); + if ( chunks == NULL ) return NULL; + + // This should rather be at the point of no return + va_list ap; + va_start( ap, count ); + + // Create chunks + for ( i = 0; i < count && !failure; i++ ) + { + // Get value type + vtype = va_arg( ap, enum mkjson_value_type ); + + // Get key + if ( otype == MKJSON_OBJ ) + { + key = va_arg( ap, char* ); + if ( key == NULL ) + { + failure = 1; + break; + } + } + else key = ""; + + // Generate prefix + if ( allsprintf( &prefix, "%s%s%s", + otype == MKJSON_OBJ ? "\"" : "", // Quote before key + key, // Key + otype == MKJSON_OBJ ? "\": " : "" ) == -1 ) // Quote and colon after key + { + failure = 1; + break; + } + + // Depending on value type + ign = 0; + switch ( vtype ) + { + // Ignore string / JSON data + case MKJSON_IGN_STRING: + case MKJSON_IGN_JSON: + (void) va_arg( ap, const char* ); + ign = 1; + break; + + // Ignore string / JSON data and pass the pointer to free + case MKJSON_IGN_STRING_FREE: + case MKJSON_IGN_JSON_FREE: + free( va_arg( ap, char* ) ); + ign = 1; + break; + + // Ignore int / long long int + case MKJSON_IGN_INT: + case MKJSON_IGN_LLINT: + if ( vtype == MKJSON_IGN_INT ) + (void) va_arg( ap, int ); + else + (void) va_arg( ap, long long int ); + ign = 1; + break; + + // Ignore double / long double + case MKJSON_IGN_DOUBLE: + case MKJSON_IGN_LDOUBLE: + if ( vtype == MKJSON_IGN_DOUBLE ) + (void) va_arg( ap, double ); + else + (void) va_arg( ap, long double ); + ign = 1; + break; + + // Ignore boolean + case MKJSON_IGN_BOOL: + (void) va_arg( ap, int ); + ign = 1; + break; + + // Ignore null value + case MKJSON_IGN_NULL: + ign = 1; + break; + + // A null-terminated string + case MKJSON_STRING: + case MKJSON_STRING_FREE: + strval = va_arg( ap, const char* ); + + // If the pointer points to NULL, the string will be replaced with JSON null value + if ( strval == NULL ) + { + if ( allsprintf( chunks + i, "%snull", prefix ) == -1 ) + chunks[i] = NULL; + } + else + { + if ( allsprintf( chunks + i, "%s\"%s\"", prefix, strval ) == -1 ) + chunks[i] = NULL; + } + + // Optional free + if ( vtype == MKJSON_STRING_FREE ) + free( (char*) strval ); + break; + + // Embed JSON data + case MKJSON_JSON: + case MKJSON_JSON_FREE: + strval = va_arg( ap, const char* ); + + // If the pointer points to NULL, the JSON data is replaced with null value + if ( allsprintf( chunks + i, "%s%s", prefix, strval == NULL ? "null" : strval ) == -1 ) + chunks[i] = NULL; + + // Optional free + if ( vtype == MKJSON_JSON_FREE ) + free( (char*) strval ); + break; + + // int / long long int + case MKJSON_INT: + case MKJSON_LLINT: + if ( vtype == MKJSON_INT ) + intval = va_arg( ap, int ); + else + intval = va_arg( ap, long long int ); + + if ( allsprintf( chunks + i, "%s%Ld", prefix, intval ) == -1 ) chunks[i] = NULL; + break; + + // double / long double + case MKJSON_DOUBLE: + case MKJSON_LDOUBLE: + if ( vtype == MKJSON_DOUBLE ) + dblval = va_arg( ap, double ); + else + dblval = va_arg( ap, long double ); + + if ( allsprintf( chunks + i, "%s%Lf", prefix, dblval ) == -1 ) chunks[i] = NULL; + break; + + // double / long double + case MKJSON_SCI_DOUBLE: + case MKJSON_SCI_LDOUBLE: + if ( vtype == MKJSON_SCI_DOUBLE ) + dblval = va_arg( ap, double ); + else + dblval = va_arg( ap, long double ); + + if ( allsprintf( chunks + i, "%s%Le", prefix, dblval ) == -1 ) chunks[i] = NULL; + break; + + // Boolean + case MKJSON_BOOL: + intval = va_arg( ap, int ); + if ( allsprintf( chunks + i, "%s%s", prefix, intval ? "true" : "false" ) == -1 ) chunks[i] = NULL; + break; + + // JSON null + case MKJSON_NULL: + if ( allsprintf( chunks + i, "%snull", prefix ) == -1 ) chunks[i] = NULL; + break; + + // Bad type specifier + default: + chunks[i] = NULL; + break; + } + + // Free prefix memory + free( prefix ); + + // NULL chunk without ignore flag indicates failure + if ( !ign && chunks[i] == NULL ) failure = 1; + + // NULL chunk now indicates ignore flag + if ( ign ) chunks[i] = NULL; + else goodchunks++; + } + + // We won't use ap anymore + va_end( ap ); + + // If everything is fine, merge chunks and create full JSON table + if ( !failure ) + { + // Get total length (this is without NUL byte) + len = 0; + for ( i = 0; i < count; i++ ) + if ( chunks[i] != NULL ) + len += strlen( chunks[i] ); + + // Total length = Chunks length + 2 brackets + separators + if ( goodchunks == 0 ) goodchunks = 1; + len = len + 2 + ( goodchunks - 1 ) * 2; + + // Allocate memory for the whole thing + json = calloc( len + 1, sizeof( char ) ); + if ( json != NULL ) + { + // Merge chunks (and do not overwrite the first bracket) + for ( i = 0; i < count; i++ ) + { + // Add separators: + // - not on the begining + // - always after valid chunk + // - between two valid chunks + // - between valid and ignored chunk if the latter isn't the last one + if ( i != 0 && chunks[i - 1] != NULL && ( chunks[i] != NULL || ( chunks[i] == NULL && i != count - 1 ) ) ) + strcat( json + 1, ", "); + + if ( chunks[i] != NULL ) + strcat( json + 1, chunks[i] ); + } + + // Add proper brackets + json[0] = otype == MKJSON_OBJ ? '{' : '['; + json[len - 1] = otype == MKJSON_OBJ ? '}' : ']'; + } + } + else json = NULL; + + // Free chunks + for ( i = 0; i < count; i++ ) + free( chunks[i] ); + free( chunks ); + + return json; +} + diff --git a/src/shim/mkjson/mkjson.h b/src/shim/mkjson/mkjson.h new file mode 100644 index 000000000..38cc07b26 --- /dev/null +++ b/src/shim/mkjson/mkjson.h @@ -0,0 +1,50 @@ +/* mkjson.h - a part of mkjson library + * + * Copyright (C) 2018 Jacek Wieczorek + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#ifndef MKJSON_H +#define MKJSON_H + +// JSON container types +enum mkjson_container_type +{ + MKJSON_ARR = 0, // An array + MKJSON_OBJ = 1 // An object (hash or whatever you call it) +}; + +// JSON data types +enum mkjson_value_type +{ + MKJSON_STRING = (int)('s'), // const char* - String data + MKJSON_STRING_FREE = (int)('f'), // char* - String data, but pointer is freed + MKJSON_JSON = (int)('r'), // const char* - JSON data (like string, but no quotes) + MKJSON_JSON_FREE = (int)('j'), // char* - JSON data, but pointer is freed + MKJSON_INT = (int)('i'), // int - An integer + MKJSON_LLINT = (int)('I'), // long long int - A long integer + MKJSON_DOUBLE = (int)('d'), // double - A double + MKJSON_LDOUBLE = (int)('D'), // long double - A long double + MKJSON_SCI_DOUBLE = (int)('e'), // double - A double with scientific notation + MKJSON_SCI_LDOUBLE = (int)('E'), // long double - A long double with scientific notation + MKJSON_BOOL = (int)('b'), // int - A boolean value + MKJSON_NULL = (int)('n'), // -- - JSON null value + + // These cause one argument of certain type to be ignored + MKJSON_IGN_STRING = (-MKJSON_STRING), + MKJSON_IGN_STRING_FREE = (-MKJSON_STRING_FREE), + MKJSON_IGN_JSON = (-MKJSON_JSON), + MKJSON_IGN_JSON_FREE = (-MKJSON_JSON_FREE), + MKJSON_IGN_INT = (-MKJSON_INT), + MKJSON_IGN_LLINT = (-MKJSON_LLINT), + MKJSON_IGN_DOUBLE = (-MKJSON_DOUBLE), + MKJSON_IGN_LDOUBLE = (-MKJSON_LDOUBLE), + MKJSON_IGN_BOOL = (-MKJSON_BOOL), + MKJSON_IGN_NULL = (-MKJSON_NULL) +}; + +extern char *mkjson( enum mkjson_container_type otype, int count, ... ); + +#endif diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c new file mode 100644 index 000000000..8b6feab99 --- /dev/null +++ b/src/shim/vyshim.c @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2020 VyOS maintainers and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or later as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <string.h> +#include <sys/time.h> +#include <time.h> +#include <stdint.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <zmq.h> +#include "mkjson.h" + +/* + * + * + */ + +#if DEBUG +#define DEBUG_ON 1 +#else +#define DEBUG_ON 0 +#endif +#define debug_print(fmt, ...) \ + do { if (DEBUG_ON) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0) +#define debug_call(f) \ + do { if (DEBUG_ON) f; } while (0) + +#define SOCKET_PATH "ipc:///run/vyos-configd.sock" + +#define GET_ACTIVE "cli-shell-api --show-active-only --show-show-defaults --show-ignore-edit showConfig" +#define GET_SESSION "cli-shell-api --show-working-only --show-show-defaults --show-ignore-edit showConfig" + +#define COMMIT_MARKER "/var/tmp/initial_in_commit" + +enum { + SUCCESS = 1 << 0, + ERROR_COMMIT = 1 << 1, + ERROR_DAEMON = 1 << 2, + PASS = 1 << 3 +}; + +volatile int init_alarm = 0; +volatile int timeout = 0; + +int initialization(void *); +int pass_through(char **, int); +void timer_handler(int); + +double get_posix_clock_time(void); + +int main(int argc, char* argv[]) +{ + // string for node data: conf_mode script and tagnode, if applicable + char string_node_data[256]; + string_node_data[0] = '\0'; + + void *context = zmq_ctx_new(); + void *requester = zmq_socket(context, ZMQ_REQ); + + int init_timeout = 0; + + debug_print("Connecting to vyos-configd ...\n"); + zmq_connect(requester, SOCKET_PATH); + + if (access(COMMIT_MARKER, F_OK) != -1) { + init_timeout = initialization(requester); + if (!init_timeout) remove(COMMIT_MARKER); + } + + int end = argc > 3 ? 2 : argc - 1; + + // if initial communication failed, pass through execution of script + if (init_timeout) { + int ret = pass_through(argv, end); + return ret; + } + + for (int i = end; i > 0 ; i--) { + strncat(&string_node_data[0], argv[i], 127); + } + + char error_code[1]; + debug_print("Sending node data ...\n"); + char *string_node_data_msg = mkjson(MKJSON_OBJ, 2, + MKJSON_STRING, "type", "node", + MKJSON_STRING, "data", &string_node_data[0]); + + zmq_send(requester, string_node_data_msg, strlen(string_node_data_msg), 0); + zmq_recv(requester, error_code, 1, 0); + debug_print("Received node data receipt\n"); + + int err = (int)error_code[0]; + + free(string_node_data_msg); + + zmq_close(requester); + zmq_ctx_destroy(context); + + if (err & PASS) { + debug_print("Received PASS\n"); + int ret = pass_through(argv, end); + return ret; + } + + if (err & ERROR_DAEMON) { + debug_print("Received ERROR_DAEMON\n"); + int ret = pass_through(argv, end); + return ret; + } + + if (err & ERROR_COMMIT) { + debug_print("Received ERROR_COMMIT\n"); + return -1; + } + + return 0; +} + +int initialization(void* Requester) +{ + char *active_str = NULL; + size_t active_len = 0; + + char *session_str = NULL; + size_t session_len = 0; + + char *empty_string = "\n"; + + char buffer[16]; + + struct sigaction sa; + struct itimerval timer, none_timer; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = &timer_handler; + sigaction(SIGALRM, &sa, NULL); + + timer.it_value.tv_sec = 0; + timer.it_value.tv_usec = 10000; + timer.it_interval.tv_sec = timer.it_interval.tv_usec = 0; + none_timer.it_value.tv_sec = none_timer.it_value.tv_usec = 0; + none_timer.it_interval.tv_sec = none_timer.it_interval.tv_usec = 0; + + double prev_time_value, time_value; + double time_diff; + + debug_print("Sending init announcement\n"); + char *init_announce = mkjson(MKJSON_OBJ, 1, + MKJSON_STRING, "type", "init"); + + // check for timeout on initial contact + while (!init_alarm) { + debug_call(prev_time_value = get_posix_clock_time()); + + setitimer(ITIMER_REAL, &timer, NULL); + + zmq_send(Requester, init_announce, strlen(init_announce), 0); + zmq_recv(Requester, buffer, 16, 0); + + setitimer(ITIMER_REAL, &none_timer, &timer); + + debug_call(time_value = get_posix_clock_time()); + + debug_print("Received init receipt\n"); + debug_call(time_diff = time_value - prev_time_value); + debug_print("time elapse %f\n", time_diff); + + break; + } + + free(init_announce); + + if (timeout) return -1; + + FILE *fp_a = popen(GET_ACTIVE, "r"); + getdelim(&active_str, &active_len, '\0', fp_a); + int ret = pclose(fp_a); + + if (!ret) { + debug_print("Sending active config\n"); + zmq_send(Requester, active_str, active_len - 1, 0); + zmq_recv(Requester, buffer, 16, 0); + debug_print("Received active receipt\n"); + } else { + debug_print("Sending empty active config\n"); + zmq_send(Requester, empty_string, 0, 0); + zmq_recv(Requester, buffer, 16, 0); + debug_print("Received active receipt\n"); + } + + free(active_str); + + FILE *fp_s = popen(GET_SESSION, "r"); + getdelim(&session_str, &session_len, '\0', fp_s); + pclose(fp_s); + + debug_print("Sending session config\n"); + zmq_send(Requester, session_str, session_len - 1, 0); + zmq_recv(Requester, buffer, 16, 0); + debug_print("Received session receipt\n"); + + free(session_str); + + return 0; +} + +int pass_through(char **argv, int end) +{ + char *newargv[] = { NULL, NULL }; + pid_t child_pid; + + newargv[0] = argv[end]; + if (end > 1) { + putenv(argv[end - 1]); + } + + debug_print("pass-through invoked\n"); + + if ((child_pid=fork()) < 0) { + debug_print("fork() failed\n"); + return -1; + } else if (child_pid == 0) { + if (-1 == execv(argv[end], newargv)) { + debug_print("pass_through execve failed %s: %s\n", + argv[end], strerror(errno)); + return -1; + } + } else if (child_pid > 0) { + int status; + pid_t wait_pid = waitpid(child_pid, &status, 0); + if (wait_pid < 0) { + debug_print("waitpid() failed\n"); + return -1; + } else if (wait_pid == child_pid) { + if (WIFEXITED(status)) { + debug_print("child exited with code %d\n", + WEXITSTATUS(status)); + return WEXITSTATUS(status); + } + } + } + + return 0; +} + +void timer_handler(int signum) +{ + debug_print("timer_handler invoked\n"); + timeout = 1; + init_alarm = 1; + + return; +} + +#ifdef _POSIX_MONOTONIC_CLOCK +double get_posix_clock_time(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + return (double) (ts.tv_sec + ts.tv_nsec / 1000000000.0); + } else { + return 0; + } +} +#else +double get_posix_clock_time(void) +{return (double)0;} +#endif diff --git a/src/systemd/lcdproc.service b/src/systemd/lcdproc.service new file mode 100644 index 000000000..ef717667a --- /dev/null +++ b/src/systemd/lcdproc.service @@ -0,0 +1,13 @@ +[Unit] +Description=LCDproc system status information viewer on %I +Documentation=man:lcdproc(8) http://www.lcdproc.org/ +After=vyos-router.service LCDd.service +Requires=LCDd.service + +[Service] +User=root +ExecStart=/usr/bin/lcdproc -f -c /run/lcdproc/lcdproc.conf +PIDFile=/run/lcdproc/lcdproc.pid + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-configd.service b/src/systemd/vyos-configd.service new file mode 100644 index 000000000..274ccc787 --- /dev/null +++ b/src/systemd/vyos-configd.service @@ -0,0 +1,27 @@ +[Unit] +Description=VyOS configuration daemon + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle +DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-configd needs is read/write mounted root +After=systemd-remount-fs.service +Before=vyos-router.service + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-configd +Type=idle + +SyslogIdentifier=vyos-configd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work in Jessie but leave it here +User=root +Group=vyattacfg + +[Install] +WantedBy=vyos.target diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service index 21e189e4b..7e0bee8e1 100644 --- a/src/systemd/wpa_supplicant-macsec@.service +++ b/src/systemd/wpa_supplicant-macsec@.service @@ -1,17 +1,17 @@ -[Unit]
-Description=WPA supplicant daemon (macsec-specific version)
-Requires=sys-subsystem-net-devices-%i.device
-ConditionPathExists=/run/wpa_supplicant/%I.conf
-After=vyos-router.service
-RequiresMountsFor=/run
-
-# NetworkManager users will probably want the dbus version instead.
-
-[Service]
-Type=simple
-WorkingDirectory=/run/wpa_supplicant
-PIDFile=/run/wpa_supplicant/%I.pid
-ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
-
-[Install]
-WantedBy=multi-user.target
+[Unit] +Description=WPA supplicant daemon (macsec-specific version) +Requires=sys-subsystem-net-devices-%i.device +ConditionPathExists=/run/wpa_supplicant/%I.conf +After=vyos-router.service +RequiresMountsFor=/run + +# NetworkManager users will probably want the dbus version instead. + +[Service] +Type=simple +WorkingDirectory=/run/wpa_supplicant +PIDFile=/run/wpa_supplicant/%I.pid +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py index c4c59b827..1597025e8 100644 --- a/src/tests/test_initial_setup.py +++ b/src/tests/test_initial_setup.py @@ -21,6 +21,7 @@ import tempfile import unittest from unittest import TestCase, mock +from vyos import xml import vyos.configtree import vyos.initialsetup as vis @@ -30,6 +31,7 @@ class TestInitialSetup(TestCase): with open('tests/data/config.boot.default', 'r') as f: config_string = f.read() self.config = vyos.configtree.ConfigTree(config_string) + self.xml = xml.load_configuration() def test_set_user_password(self): vis.set_user_password(self.config, 'vyos', 'vyosvyos') @@ -56,7 +58,7 @@ class TestInitialSetup(TestCase): self.assertEqual(key_type, 'ssh-rsa') self.assertEqual(key_data, 'fakedata') - self.assertTrue(self.config.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) def test_set_ssh_key_without_name(self): # If key file doesn't include a name, the function will use user name for the key name @@ -69,7 +71,7 @@ class TestInitialSetup(TestCase): self.assertEqual(key_type, 'ssh-rsa') self.assertEqual(key_data, 'fakedata') - self.assertTrue(self.config.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) def test_create_user(self): vis.create_user(self.config, 'jrandomhacker', password='qwerty', key=" ssh-rsa fakedata jrandomhacker@foovax ") @@ -95,8 +97,8 @@ class TestInitialSetup(TestCase): vis.set_default_gateway(self.config, '192.0.2.1') self.assertTrue(self.config.exists(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '192.0.2.1'])) - self.assertTrue(self.config.is_tag(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop'])) - self.assertTrue(self.config.is_tag(['protocols', 'static', 'route'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route'])) if __name__ == "__main__": unittest.main() diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal new file mode 100755 index 000000000..652110346 --- /dev/null +++ b/src/validators/dotted-decimal @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +area = sys.argv[1] + +res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area) +if not res: + print("\'{0}\' is not a valid dotted decimal value".format(area)) + sys.exit(1) +else: + components = res.groups() + for n in range(0, 4): + if (int(components[n]) > 255): + print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n])) + sys.exit(1) + +sys.exit(0) |