sifi #5
|
|
@ -0,0 +1,8 @@
|
|||
download_url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/oxlorg-opnsense-25.7.8.tar.gz
|
||||
format_version: 1.0.0
|
||||
name: opnsense
|
||||
namespace: oxlorg
|
||||
server: https://galaxy.ansible.com/api/
|
||||
signatures: []
|
||||
version: 25.7.8
|
||||
version_url: /api/v3/plugin/ansible/content/published/collections/index/oxlorg/opnsense/versions/25.7.8/
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Contribute
|
||||
|
||||
Thank you for considering to contribute to this project!
|
||||
|
||||
## What to contribute
|
||||
|
||||
* report errors as [issues](https://github.com/O-X-L/ansible-opnsense/issues)
|
||||
* test unstable modules and [report if they work as expected](https://github.com/O-X-L/ansible-opnsense/discussions/new?category=general)
|
||||
* add [ansible-based tests](https://github.com/O-X-L/ansible-opnsense/blob/latest/tests) for some error-case(s) you have encountered
|
||||
* extend or correct the [documentation](https://github.com/O-X-L/ansible-opnsense/blob/latest/docs)
|
||||
* add missing inline documentation [as standardized](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#documentation-block)
|
||||
* should be placed in `<COLLECTION>/plugins/module_utils/inline_docs/<MODULE>.py` and then imported in the module
|
||||
* contribute code fixes or optimizations
|
||||
* implement additional API endpoints/modules
|
||||
|
||||
## Module development
|
||||
|
||||
See: [Documentation - Development](https://ansible-opnsense.oxl.app/usage/4_develop.html)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,641 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If 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 convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
Ansible Collection to manage OPNsense firewalls using its API.
|
||||
Copyright (C) 2025 OXL IT Services
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
E-Mail: contact+opnsense@OXL.at
|
||||
Web: https://www.OXL.at
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"collection_info": {
|
||||
"namespace": "oxlorg",
|
||||
"name": "opnsense",
|
||||
"version": "25.7.8",
|
||||
"authors": [
|
||||
"OXL IT Services <contact+opnsense@OXL.at>"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"tags": [
|
||||
"firewall",
|
||||
"api",
|
||||
"opnsense",
|
||||
"iac",
|
||||
"network",
|
||||
"security",
|
||||
"filter"
|
||||
],
|
||||
"description": "Ansible Collection to manage OPNsense firewalls using its API",
|
||||
"license": [],
|
||||
"license_file": "LICENSE.txt",
|
||||
"dependencies": {},
|
||||
"repository": "https://github.com/O-X-L/ansible-opnsense",
|
||||
"documentation": "https://ansible-opnsense.oxl.app",
|
||||
"homepage": "https://www.OXL.at",
|
||||
"issues": "https://github.com/O-X-L/ansible-opnsense/issues"
|
||||
},
|
||||
"file_manifest_file": {
|
||||
"name": "FILES.json",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "3002a7e33b186084479c7a4765503e022dbf6d438d9de1548793d1845a8d4352",
|
||||
"format": 1
|
||||
},
|
||||
"format": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
# Ansible Collection to manage OPNsense Firewalls
|
||||
|
||||
<p align="center">
|
||||
<a title="Support this Project (Donate, Support-Licenses)" href="https://shop.oxl.app/collections/open-source">
|
||||
<img src="https://files.oxl.at/img/badge-oss-support.svg" alt="Support Badge (Donate, Support-Licenses)"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
[](https://github.com/O-X-L/ansible-opnsense/actions/workflows/lint-python.yml)
|
||||
[](https://github.com/O-X-L/ansible-opnsense/actions/workflows/lint-ansible.yml)
|
||||
[](https://github.com/O-X-L/ansible-opnsense/actions/workflows/unit_test.yml)
|
||||
[](https://galaxy.ansible.com/ui/repo/published/oxlorg/opnsense)
|
||||
|
||||
**Functional Tests**:
|
||||
|
||||
* Status: [](https://github.com/O-X-L/ansible-opnsense/actions/workflows/functional_test_result.yml) |
|
||||
[](https://github.com/O-X-L/ansible-opnsense/actions/workflows/functional_test_result.yml)
|
||||
* Logs: [API](https://ci.oss.oxl.app/api/job/ansible-test-collection-opnsense/logs?token=2b7bba30-9a37-4b57-be8a-99e23016ce70&lines=1000) |
|
||||
[Daily Archive](https://github.com/O-X-L/ansible-opnsense/actions/workflows/functional_test_result.yml)
|
||||
|
||||
Internal CI: [Tester Role](https://github.com/O-X-L/ansible-role-oxl-cicd) | [Jobs API](https://github.com/O-X-L/github-self-hosted-jobs-systemd)
|
||||
|
||||
----
|
||||
|
||||
## Requirements
|
||||
|
||||
The [httpx python module](https://www.python-httpx.org/) is used for API communications!
|
||||
|
||||
```bash
|
||||
python3 -m pip install --upgrade httpx
|
||||
```
|
||||
|
||||
Then - install the collection itself:
|
||||
|
||||
```bash
|
||||
# latest version:
|
||||
ansible-galaxy collection install git+https://github.com/O-X-L/ansible-opnsense.git
|
||||
|
||||
# stable/tested version:
|
||||
ansible-galaxy collection install git+https://github.com/O-X-L/ansible-opnsense.git,25.7.8
|
||||
## OR
|
||||
ansible-galaxy collection install oxlorg.opnsense
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
## Usage
|
||||
|
||||
See: [Docs](https://ansible-opnsense.oxl.app)
|
||||
|
||||
[](https://status.oxl.at/endpoints/1--oxl_opnsense-ansible-collection-docs)
|
||||
|
||||
If you DO NOT want to use Ansible - [this fork](https://github.com/O-X-L/opnsense-api-client) provides you with a raw Python3 interface.
|
||||
|
||||
----
|
||||
|
||||
## Support the project(s)
|
||||
|
||||
Support the Open-Source projects that make these modules possible:
|
||||
|
||||
* [Donate to OPNsense](https://opnsense.org/donate/) or [Buy the Business-Edition](https://shop.opnsense.com/product-categorie/software_and_licenses/)
|
||||
* [Donate to the Ansible-Module Maintainers](https://shop.oxl.app/products/open-source-spende) or [Buy a Support-License](https://shop.oxl.app/products/open-source-support-opnsense-ansible-collection)
|
||||
|
||||
----
|
||||
|
||||
## Contribute
|
||||
|
||||
Feel free to contribute to this project using [pull-requests](https://github.com/O-X-L/ansible-opnsense/pulls), [issues](https://github.com/O-X-L/ansible-opnsense/issues) and [discussions](https://github.com/O-X-L/ansible-opnsense/discussions)!
|
||||
|
||||
See also: [Contributing](https://github.com/O-X-L/ansible-opnsense/blob/latest/CONTRIBUTING.md)
|
||||
|
||||
<img src="https://contrib.rocks/image?repo=O-X-L/ansible-opnsense&max=7" />
|
||||
|
||||
----
|
||||
|
||||
## Version Support
|
||||
|
||||
We try that the `oxlorg.opnsense` modules always support the latest version of OPNsense.
|
||||
|
||||
If an API changed, the current module-implementation might fail for firewalls running an older firmware.
|
||||
|
||||
As [this project is unfunded](https://github.com/O-X-L/ansible-opnsense/discussions/199) we do not actively check for API-changes - if you find missing functionalities you need/want to have please [report it](https://github.com/O-X-L/ansible-opnsense/issues)!
|
||||
|
||||
----
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
**Development States**:
|
||||
|
||||
not implemented => development => [testing](https://github.com/O-X-L/ansible-opnsense/tree/latest/tests) => [unstable (_practical testing_)](https://github.com/O-X-L/ansible-opnsense/discussions/85) => stable
|
||||
|
||||
### Implemented
|
||||
|
||||
|
||||
| Function | Module | Usage | State |
|
||||
|:--------------------------|:---------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------|:---------|
|
||||
| **Base** | oxlorg.opnsense.list | [Docs](https://ansible-opnsense.oxl.app/general/list.html) | stable |
|
||||
| **Base** | oxlorg.opnsense.reload | [Docs](https://ansible-opnsense.oxl.app/general/reload.html) | stable |
|
||||
| **Raw** | oxlorg.opnsense.raw | [Docs](https://ansible-opnsense.oxl.app/general/raw.html) | unstable |
|
||||
| **Services** | oxlorg.opnsense.service | [Docs](https://ansible-opnsense.oxl.app/general/service.html) | stable |
|
||||
| **Alias** | oxlorg.opnsense.alias | [Docs](https://ansible-opnsense.oxl.app/modules/alias.html) | stable |
|
||||
| **Alias** | oxlorg.opnsense.alias_multi | [Docs](https://ansible-opnsense.oxl.app/modules/alias_multi.html) | stable |
|
||||
| **Alias** | oxlorg.opnsense.alias_purge | [Docs](https://ansible-opnsense.oxl.app/modules/alias_multi.html#oxlorg-opnsense-alias-purge) | unstable |
|
||||
| **Rules** | oxlorg.opnsense.rule | [Docs](https://ansible-opnsense.oxl.app/modules/rule.html) | stable |
|
||||
| **Rules** | oxlorg.opnsense.rule_multi | [Docs](https://ansible-opnsense.oxl.app/modules/rule_multi.html) | stable |
|
||||
| **Rules** | oxlorg.opnsense.rule_purge | [Docs](https://ansible-opnsense.oxl.app/modules/rule_multi.html#oxlorg-opnsense-rule-purge) | unstable |
|
||||
| **Rule Interface Groups** | oxlorg.opnsense.rule_interface_group | [Docs](https://ansible-opnsense.oxl.app/modules/rule_interface_group.html#oxlorg-opnsense-rule-interface-group) | stable |
|
||||
| **Savepoints** | oxlorg.opnsense.savepoint | [Docs](https://ansible-opnsense.oxl.app/modules/savepoint.html) | stable |
|
||||
| **Packages** | oxlorg.opnsense.package | [Docs](https://ansible-opnsense.oxl.app/modules/package.html) | stable |
|
||||
| **System** | oxlorg.opnsense.system | [Docs](https://ansible-opnsense.oxl.app/modules/system.html) | stable |
|
||||
| **Cron-Jobs** | oxlorg.opnsense.cron | [Docs](https://ansible-opnsense.oxl.app/modules/cron.html) | stable |
|
||||
| **Routes** | oxlorg.opnsense.route | [Docs](https://ansible-opnsense.oxl.app/modules/routing.html) | stable |
|
||||
| **Gateways** | oxlorg.opnsense.gateway | [Docs](https://ansible-opnsense.oxl.app/modules/routing.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_general | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_general.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_acl | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_acl.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_forward | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_forwarding.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_dot | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_dot.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_host | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_host.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_host_alias | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_host_alias.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.unbound_dnsbl | [Docs](https://ansible-opnsense.oxl.app/modules/unbound_host_alias.html) | stable |
|
||||
| **Syslog** | oxlorg.opnsense.syslog | [Docs](https://ansible-opnsense.oxl.app/modules/syslog.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_connection, oxlorg.opnsense.ipsec_tunnel | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_pool, oxlorg.opnsense.ipsec_network | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_auth_local | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_auth_remote | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_child | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_vti | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_cert | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_psk | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.ipsec_manual_spd | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | stable |
|
||||
| **IPSec** | oxlorg.opnsense.general | [Docs](https://ansible-opnsense.oxl.app/modules/ipsec.html) | unstable |
|
||||
| **Traffic Shaper** | oxlorg.opnsense.shaper_pipe | [Docs](https://ansible-opnsense.oxl.app/modules/shaper.html) | stable |
|
||||
| **Traffic Shaper** | oxlorg.opnsense.shaper_queue | [Docs](https://ansible-opnsense.oxl.app/modules/shaper.html) | stable |
|
||||
| **Traffic Shaper** | oxlorg.opnsense.shaper_rule | [Docs](https://ansible-opnsense.oxl.app/modules/shaper.html) | stable |
|
||||
| **Monit** | oxlorg.opnsense.monit_service | [Docs](https://ansible-opnsense.oxl.app/modules/monit.html) | stable |
|
||||
| **Monit** | oxlorg.opnsense.monit_alert | [Docs](https://ansible-opnsense.oxl.app/modules/monit.html) | stable |
|
||||
| **Monit** | oxlorg.opnsense.monit_test | [Docs](https://ansible-opnsense.oxl.app/modules/monit.html) | stable |
|
||||
| **WireGuard** | oxlorg.opnsense.wireguard_server | [Docs](https://ansible-opnsense.oxl.app/modules/wireguard.html) | stable |
|
||||
| **WireGuard** | oxlorg.opnsense.wireguard_peer | [Docs](https://ansible-opnsense.oxl.app/modules/wireguard.html) | stable |
|
||||
| **WireGuard** | oxlorg.opnsense.wireguard_show | [Docs](https://ansible-opnsense.oxl.app/modules/wireguard.html) | stable |
|
||||
| **WireGuard** | oxlorg.opnsense.wireguard_general | [Docs](https://ansible-opnsense.oxl.app/modules/wireguard.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_vlan | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_vxlan | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_vip | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_lagg | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_loopback | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_gre | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | stable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_bridge | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | unstable |
|
||||
| **Interfaces** | oxlorg.opnsense.interface_gif | [Docs](https://ansible-opnsense.oxl.app/modules/interface.html) | unstable |
|
||||
| **NAT** | oxlorg.opnsense.nat_source | [Docs](https://ansible-opnsense.oxl.app/modules/source_nat.html) | stable |
|
||||
| **NAT** | oxlorg.opnsense.nat_one_to_one | [Docs](https://ansible-opnsense.oxl.app/modules/one_to_one.html) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_diagnostic | [Docs](https://ansible-opnsense.oxl.app/modules/frr_diagnostic.html) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_general | [Docs](https://ansible-opnsense.oxl.app/modules/frr_general.html) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bfd_general | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bfd.html#oxlorg-opnsense-frr-bfd-general) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bfd_neighbor | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bfd.html#oxlorg-opnsense-frr-bfd-neighbor) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_general | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-general) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_neighbor | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-neighbor) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_prefix_list | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-prefix-list) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_route_map | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-route-map) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_community_list | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-community-list) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_as_path | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-as-path) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_redistribution | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-redistribution) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_bgp_peer_group | [Docs](https://ansible-opnsense.oxl.app/modules/frr_bgp.html#oxlorg-opnsense-frr-bgp-peer-group) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_general | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-general) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_prefix_list | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-prefix-list) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_route_map | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-route-map) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_interface | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-interface) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_network | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-network) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf_redistribution | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf-redistribution) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_general | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-general) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_prefix_list | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-prefix-list) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_route_map | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-route-map) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_interface | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-interface) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_network | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-network) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_ospf3_redistribution | [Docs](https://ansible-opnsense.oxl.app/modules/frr_ospf.html#oxlorg-opnsense-frr-ospf3-redistribution) | stable |
|
||||
| **Dynamic Routing** | oxlorg.opnsense.frr_rip | [Docs](https://ansible-opnsense.oxl.app/modules/frr_rip.html) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_general | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-general) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_blocklist | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-blocklist) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_acl | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-acl) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_domain | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-domain) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_record | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-record) | stable |
|
||||
| **DNS** | oxlorg.opnsense.bind_record_multi | [Docs](https://ansible-opnsense.oxl.app/modules/bind.html#oxlorg-opnsense-bind-record-multi) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_general | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id2) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_cache | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id3) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_parent | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id4) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_traffic | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id5) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_forward | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id7) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_acl | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id8) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_icap | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id9) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_auth | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id10) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_remote_acl | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id12) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_pac_proxy | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id14) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_pac_match | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id15) | stable |
|
||||
| **Web Proxy** | oxlorg.opnsense.webproxy_pac_rule | [Docs](https://ansible-opnsense.oxl.app/modules/webproxy.html#id18) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_action | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id2) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_general | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id3) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_ruleset | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id4) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_rule | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id5) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_user_rule | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id6) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_policy | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id7) | stable |
|
||||
| **IDS/IPS** | oxlorg.opnsense.ids_policy_rule | [Docs](https://ansible-opnsense.oxl.app/modules/ids.html#id8) | stable |
|
||||
| **OpenVPN** | oxlorg.opnsense.openvpn_client | [Docs](https://ansible-opnsense.oxl.app/modules/openvpn.html) | stable |
|
||||
| **OpenVPN** | oxlorg.opnsense.openvpn_server | [Docs](https://ansible-opnsense.oxl.app/modules/openvpn.html) | stable |
|
||||
| **OpenVPN** | oxlorg.opnsense.openvpn_static_key | [Docs](https://ansible-opnsense.oxl.app/modules/openvpn.html) | stable |
|
||||
| **OpenVPN** | oxlorg.opnsense.openvpn_status | [Docs](https://ansible-opnsense.oxl.app/modules/openvpn.html) | stable |
|
||||
| **OpenVPN** | oxlorg.opnsense.openvpn_client_override | [Docs](https://ansible-opnsense.oxl.app/modules/openvpn.html) | stable |
|
||||
| **Nginx** | oxlorg.opnsense.nginx_general | [Docs](https://ansible-opnsense.oxl.app/modules/nginx.html#oxlorg-opnsense-nginx-general) | stable |
|
||||
| **Nginx** | oxlorg.opnsense.nginx_upstream_server | [Docs](https://ansible-opnsense.oxl.app/modules/nginx.html#oxlorg-opnsense-nginx-upstream-server) | stable |
|
||||
| **DHCP Relay** | oxlorg.opnsense.dhcrelay_relay | [Docs](https://ansible-opnsense.oxl.app/modules/dhcrelay_relay.html) | stable |
|
||||
| **DHCP Relay** | oxlorg.opnsense.dhcrelay_destination | [Docs](https://ansible-opnsense.oxl.app/modules/dhcrelay_destination.html) | stable |
|
||||
| **DHCP** | oxlorg.opnsense.dhcp_general | [Docs](https://ansible-opnsense.oxl.app/modules/dhcp.html) | stable |
|
||||
| **DHCP Subnet** | oxlorg.opnsense.dhcp_subnet | [Docs](https://ansible-opnsense.oxl.app/modules/dhcp.html) | stable |
|
||||
| **DHCP Reservation** | oxlorg.opnsense.dhcp_reservation | [Docs](https://ansible-opnsense.oxl.app/modules/dhcp.html) | stable |
|
||||
| **DHCP Controlagent** | oxlorg.opnsense.dhcp_controlagent | [Docs](https://ansible-opnsense.oxl.app/modules/dhcp.html) | stable |
|
||||
| **ACME (Certificates)** | oxlorg.opnsense.acme_account | [Docs](https://ansible-opnsense.oxl.app/modules/acmeclient.html) | stable |
|
||||
| **ACME (Certificates)** | oxlorg.opnsense.acme_action | [Docs](https://ansible-opnsense.oxl.app/modules/acmeclient.html) | stable |
|
||||
| **ACME (Certificates)** | oxlorg.opnsense.acme_general | [Docs](https://ansible-opnsense.oxl.app/modules/acmeclient.html) | stable |
|
||||
| **ACME (Certificates)** | oxlorg.opnsense.acme_validation | [Docs](https://ansible-opnsense.oxl.app/modules/acmeclient.html) | stable |
|
||||
| **ACME (Certificates)** | oxlorg.opnsense.acme_certificate | [Docs](https://ansible-opnsense.oxl.app/modules/acmeclient.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_general | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_domain | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_recipient | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_recipientbcc | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_sender | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_senderbcc | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_sendercanonical | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_headercheck | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Postfix** | oxlorg.opnsense.postfix_address | [Docs](https://ansible-opnsense.oxl.app/modules/postfix.html) | stable |
|
||||
| **Snapshot** | oxlorg.opnsense.snapshot | [Docs](https://ansible-opnsense.oxl.app/modules/snapshot.html) | stable |
|
||||
| **High Availability** | oxlorg.opnsense.hasync_general | [Docs](https://ansible-opnsense.oxl.app/modules/hasync.html) | stable |
|
||||
| **High Availability** | oxlorg.opnsense.hasync_service | [Docs](https://ansible-opnsense.oxl.app/modules/hasync.html) | stable |
|
||||
| **User Management** | oxlorg.opnsense.user | [Docs](https://ansible-opnsense.oxl.app/modules/access.html) | unstable |
|
||||
| **User Management** | oxlorg.opnsense.group | [Docs](https://ansible-opnsense.oxl.app/modules/access.html) | unstable |
|
||||
| **User Management** | oxlorg.opnsense.privilege | [Docs](https://ansible-opnsense.oxl.app/modules/access.html) | unstable |
|
||||
| **Neighbor** | oxlorg.opnsense.neighbor | [Docs](https://ansible-opnsense.oxl.app/modules/neighbor.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_general | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_domain | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_host | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_range | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_option | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_boot | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **Dnsmasq** | oxlorg.opnsense.dnsmasq_tag | [Docs](https://ansible-opnsense.oxl.app/modules/dnsmasq.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_cache | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_defaults | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_logging | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_peers | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_settings | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_stats | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_general_tuning | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_cpu | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_user | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_group | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_maintenance | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_acl | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_lua | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_action | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_errorfile | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
| **HAProxy** | oxlorg.opnsense.haproxy_fcgi | [Docs](https://ansible-opnsense.oxl.app/modules/haproxy.html) | unstable |
|
||||
|
||||
### Roadmap
|
||||
|
||||
See: [Feature Requests](https://github.com/O-X-L/ansible-opnsense/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement)
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
---
|
||||
|
||||
requires_ansible: ">=2.14"
|
||||
|
||||
action_groups:
|
||||
alias:
|
||||
- oxlorg.opnsense.alias
|
||||
- oxlorg.opnsense.alias_multi
|
||||
- oxlorg.opnsense.alias_purge
|
||||
rule:
|
||||
- oxlorg.opnsense.rule
|
||||
- oxlorg.opnsense.rule_multi
|
||||
- oxlorg.opnsense.rule_purge
|
||||
- oxlorg.opnsense.rule_interface_group
|
||||
unbound:
|
||||
- oxlorg.opnsense.unbound_general
|
||||
- oxlorg.opnsense.unbound_acl
|
||||
- oxlorg.opnsense.unbound_forward
|
||||
- oxlorg.opnsense.unbound_dot
|
||||
- oxlorg.opnsense.unbound_host
|
||||
- oxlorg.opnsense.unbound_host_alias
|
||||
- oxlorg.opnsense.unbound_dnsbl
|
||||
ipsec:
|
||||
- oxlorg.opnsense.ipsec_auth_local
|
||||
- oxlorg.opnsense.ipsec_auth_remote
|
||||
- oxlorg.opnsense.ipsec_child
|
||||
- oxlorg.opnsense.ipsec_vti
|
||||
- oxlorg.opnsense.ipsec_cert
|
||||
- oxlorg.opnsense.ipsec_psk
|
||||
- oxlorg.opnsense.ipsec_connection
|
||||
- oxlorg.opnsense.ipsec_pool
|
||||
- oxlorg.opnsense.ipsec_manual_spd
|
||||
- oxlorg.opnsense.ipsec_general
|
||||
shaper:
|
||||
- oxlorg.opnsense.shaper_pipe
|
||||
- oxlorg.opnsense.shaper_queue
|
||||
- oxlorg.opnsense.shaper_rule
|
||||
monit:
|
||||
- oxlorg.opnsense.monit_service
|
||||
- oxlorg.opnsense.monit_alert
|
||||
- oxlorg.opnsense.monit_test
|
||||
wireguard:
|
||||
- oxlorg.opnsense.wireguard_server
|
||||
- oxlorg.opnsense.wireguard_peer
|
||||
- oxlorg.opnsense.wireguard_show
|
||||
- oxlorg.opnsense.wireguard_general
|
||||
interface:
|
||||
- oxlorg.opnsense.interface_vlan
|
||||
- oxlorg.opnsense.interface_vxlan
|
||||
- oxlorg.opnsense.interface_vip
|
||||
- oxlorg.opnsense.interface_lagg
|
||||
- oxlorg.opnsense.interface_loopback
|
||||
- oxlorg.opnsense.interface_gre
|
||||
- oxlorg.opnsense.interface_bridge
|
||||
- oxlorg.opnsense.interface_gif
|
||||
- oxlorg.opnsense.neighbor
|
||||
frr:
|
||||
- oxlorg.opnsense.frr_diagnostic
|
||||
- oxlorg.opnsense.frr_general
|
||||
- oxlorg.opnsense.frr_bfd_general
|
||||
- oxlorg.opnsense.frr_bfd_neighbor
|
||||
- oxlorg.opnsense.frr_bgp_general
|
||||
- oxlorg.opnsense.frr_bgp_prefix_list
|
||||
- oxlorg.opnsense.frr_bgp_community_list
|
||||
- oxlorg.opnsense.frr_bgp_as_path
|
||||
- oxlorg.opnsense.frr_bgp_redistribution
|
||||
- oxlorg.opnsense.frr_bgp_route_map
|
||||
- oxlorg.opnsense.frr_bgp_neighbor
|
||||
- oxlorg.opnsense.frr_bgp_peer_group
|
||||
- oxlorg.opnsense.frr_ospf_general
|
||||
- oxlorg.opnsense.frr_ospf_prefix_list
|
||||
- oxlorg.opnsense.frr_ospf_interface
|
||||
- oxlorg.opnsense.frr_ospf_redistribution
|
||||
- oxlorg.opnsense.frr_ospf_route_map
|
||||
- oxlorg.opnsense.frr_ospf_network
|
||||
- oxlorg.opnsense.frr_ospf3_general
|
||||
- oxlorg.opnsense.frr_ospf3_prefix_list
|
||||
- oxlorg.opnsense.frr_ospf3_interface
|
||||
- oxlorg.opnsense.frr_ospf3_redistribution
|
||||
- oxlorg.opnsense.frr_ospf3_route_map
|
||||
- oxlorg.opnsense.frr_ospf3_network
|
||||
- oxlorg.opnsense.frr_rip
|
||||
bind:
|
||||
- oxlorg.opnsense.bind_general
|
||||
- oxlorg.opnsense.bind_acl
|
||||
- oxlorg.opnsense.bind_blocklist
|
||||
- oxlorg.opnsense.bind_domain
|
||||
- oxlorg.opnsense.bind_record
|
||||
- oxlorg.opnsense.bind_record_multi
|
||||
webproxy:
|
||||
- oxlorg.opnsense.webproxy_general
|
||||
- oxlorg.opnsense.webproxy_cache
|
||||
- oxlorg.opnsense.webproxy_parent
|
||||
- oxlorg.opnsense.webproxy_traffic
|
||||
- oxlorg.opnsense.webproxy_forward
|
||||
- oxlorg.opnsense.webproxy_acl
|
||||
- oxlorg.opnsense.webproxy_icap
|
||||
- oxlorg.opnsense.webproxy_auth
|
||||
- oxlorg.opnsense.webproxy_remote_acl
|
||||
- oxlorg.opnsense.webproxy_pac_proxy
|
||||
- oxlorg.opnsense.webproxy_pac_match
|
||||
- oxlorg.opnsense.webproxy_pac_rule
|
||||
nginx:
|
||||
- oxlorg.opnsense.nginx_general
|
||||
- oxlorg.opnsense.nginx_upstream_server
|
||||
route:
|
||||
- oxlorg.opnsense.route
|
||||
- oxlorg.opnsense.gateway
|
||||
nat:
|
||||
- oxlorg.opnsense.nat_source
|
||||
- oxlorg.opnsense.nat_one_to_one
|
||||
system:
|
||||
- oxlorg.opnsense.list
|
||||
- oxlorg.opnsense.reload
|
||||
- oxlorg.opnsense.service
|
||||
- oxlorg.opnsense.savepoint
|
||||
- oxlorg.opnsense.package
|
||||
- oxlorg.opnsense.system
|
||||
- oxlorg.opnsense.cron
|
||||
- oxlorg.opnsense.syslog
|
||||
- oxlorg.opnsense.snapshot
|
||||
ids:
|
||||
- oxlorg.opnsense.ids_action
|
||||
- oxlorg.opnsense.ids_general
|
||||
- oxlorg.opnsense.ids_policy
|
||||
- oxlorg.opnsense.ids_policy_rule
|
||||
- oxlorg.opnsense.ids_rule
|
||||
- oxlorg.opnsense.ids_ruleset
|
||||
- oxlorg.opnsense.ids_ruleset_properties
|
||||
- oxlorg.opnsense.ids_user_rule
|
||||
openvpn:
|
||||
- oxlorg.opnsense.openvpn_client
|
||||
- oxlorg.opnsense.openvpn_server
|
||||
- oxlorg.opnsense.openvpn_static_key
|
||||
- oxlorg.opnsense.openvpn_status
|
||||
- oxlorg.opnsense.openvpn_client_override
|
||||
- oxlorg.opnsense.openvpn_client_template
|
||||
- oxlorg.opnsense.openvpn_client_export
|
||||
dnsmasq:
|
||||
- oxlorg.opnsense.dnsmasq_general
|
||||
- oxlorg.opnsense.dnsmasq_domain
|
||||
- oxlorg.opnsense.dnsmasq_host
|
||||
- oxlorg.opnsense.dnsmasq_range
|
||||
- oxlorg.opnsense.dnsmasq_option
|
||||
- oxlorg.opnsense.dnsmasq_boot
|
||||
- oxlorg.opnsense.dnsmasq_tag
|
||||
dhcrelay:
|
||||
- oxlorg.opnsense.dhcrelay_destination
|
||||
- oxlorg.opnsense.dhcrelay_relay
|
||||
dhcp:
|
||||
- oxlorg.opnsense.dhcp_reservation
|
||||
- oxlorg.opnsense.dhcp_controlagent
|
||||
- oxlorg.opnsense.dhcp_general
|
||||
- oxlorg.opnsense.dhcp_subnet
|
||||
acme:
|
||||
- oxlorg.opnsense.acme_general
|
||||
- oxlorg.opnsense.acme_account
|
||||
- oxlorg.opnsense.acme_validation
|
||||
- oxlorg.opnsense.acme_action
|
||||
- oxlorg.opnsense.acme_certificate
|
||||
postfix:
|
||||
- oxlorg.opnsense.postfix_general
|
||||
- oxlorg.opnsense.postfix_domain
|
||||
- oxlorg.opnsense.postfix_recipient
|
||||
- oxlorg.opnsense.postfix_recipientbcc
|
||||
- oxlorg.opnsense.postfix_sender
|
||||
- oxlorg.opnsense.postfix_senderbcc
|
||||
- oxlorg.opnsense.postfix_sendercanonical
|
||||
- oxlorg.opnsense.postfix_headercheck
|
||||
- oxlorg.opnsense.postfix_address
|
||||
hasync:
|
||||
- oxlorg.opnsense.hasync_general
|
||||
- oxlorg.opnsense.hasync_service
|
||||
haproxy:
|
||||
- oxlorg.opnsense.haproxy_general_settings
|
||||
- oxlorg.opnsense.haproxy_general_cache
|
||||
- oxlorg.opnsense.haproxy_general_defaults
|
||||
- oxlorg.opnsense.haproxy_general_logging
|
||||
- oxlorg.opnsense.haproxy_general_peers
|
||||
- oxlorg.opnsense.haproxy_general_stats
|
||||
- oxlorg.opnsense.haproxy_general_tuning
|
||||
- oxlorg.opnsense.haproxy_maintenance
|
||||
- oxlorg.opnsense.haproxy_cpu
|
||||
- oxlorg.opnsense.haproxy_user
|
||||
- oxlorg.opnsense.haproxy_group
|
||||
- oxlorg.opnsense.haproxy_acl
|
||||
- oxlorg.opnsense.haproxy_action
|
||||
- oxlorg.opnsense.haproxy_lua
|
||||
- oxlorg.opnsense.haproxy_fcgi
|
||||
- oxlorg.opnsense.haproxy_errorfile
|
||||
wazuh:
|
||||
- oxlorg.opnsense.wazuh_agent
|
||||
raw:
|
||||
- oxlorg.opnsense.raw
|
||||
access:
|
||||
- oxlorg.opnsense.user
|
||||
- oxlorg.opnsense.group
|
||||
- oxlorg.opnsense.privilege
|
||||
all:
|
||||
- metadata:
|
||||
extend_group:
|
||||
- oxlorg.opnsense.alias
|
||||
- oxlorg.opnsense.rule
|
||||
- oxlorg.opnsense.unbound
|
||||
- oxlorg.opnsense.ipsec
|
||||
- oxlorg.opnsense.shaper
|
||||
- oxlorg.opnsense.monit
|
||||
- oxlorg.opnsense.wireguard
|
||||
- oxlorg.opnsense.interface
|
||||
- oxlorg.opnsense.frr
|
||||
- oxlorg.opnsense.bind
|
||||
- oxlorg.opnsense.webproxy
|
||||
- oxlorg.opnsense.nginx
|
||||
- oxlorg.opnsense.route
|
||||
- oxlorg.opnsense.gateway
|
||||
- oxlorg.opnsense.nat
|
||||
- oxlorg.opnsense.system
|
||||
- oxlorg.opnsense.ids
|
||||
- oxlorg.opnsense.openvpn
|
||||
- oxlorg.opnsense.dhcrelay
|
||||
- oxlorg.opnsense.dhcp
|
||||
- oxlorg.opnsense.acme
|
||||
- oxlorg.opnsense.postfix
|
||||
- oxlorg.opnsense.hasync
|
||||
- oxlorg.opnsense.wazuh
|
||||
- oxlorg.opnsense.raw
|
||||
- oxlorg.opnsense.access
|
||||
- oxlorg.opnsense.dnsmasq
|
||||
- oxlorg.opnsense.haproxy
|
||||
|
||||
plugin_routing:
|
||||
modules:
|
||||
ipsec_tunnel:
|
||||
redirect: oxlorg.opnsense.ipsec_connection
|
||||
ipsec_network:
|
||||
redirect: oxlorg.opnsense.ipsec_pool
|
||||
snat:
|
||||
redirect: oxlorg.opnsense.source_nat
|
||||
proxy_general:
|
||||
redirect: oxlorg.opnsense.webproxy_general
|
||||
proxy_cache:
|
||||
redirect: oxlorg.opnsense.webproxy_cache
|
||||
proxy_parent:
|
||||
redirect: oxlorg.opnsense.webproxy_parent
|
||||
proxy_traffic:
|
||||
redirect: oxlorg.opnsense.webproxy_traffic
|
||||
proxy_forward:
|
||||
redirect: oxlorg.opnsense.webproxy_forward
|
||||
proxy_acl:
|
||||
redirect: oxlorg.opnsense.webproxy_acl
|
||||
proxy_icap:
|
||||
redirect: oxlorg.opnsense.webproxy_icap
|
||||
proxy_auth:
|
||||
redirect: oxlorg.opnsense.webproxy_auth
|
||||
proxy_remote_acl:
|
||||
redirect: oxlorg.opnsense.webproxy_remote_acl
|
||||
proxy_pac_proxy:
|
||||
redirect: oxlorg.opnsense.webproxy_pac_proxy
|
||||
proxy_pac_match:
|
||||
redirect: oxlorg.opnsense.webproxy_pac_match
|
||||
proxy_pac_rule:
|
||||
redirect: oxlorg.opnsense.webproxy_pac_rule
|
||||
openvpn_client_overwrite:
|
||||
redirect: oxlorg.opnsense.openvpn_client_override
|
||||
routing:
|
||||
redirect: oxlorg.opnsense.gateway
|
||||
gw:
|
||||
redirect: oxlorg.opnsense.gateway
|
||||
rule_if_group:
|
||||
redirect: oxlorg.opnsense.rule_interface_group
|
||||
dhcrelay:
|
||||
redirect: oxlorg.opnsense.dhcrelay_relay
|
||||
dhcrelay_dst:
|
||||
redirect: oxlorg.opnsense.dhcrelay_destination
|
||||
unbound_domain:
|
||||
redirect: oxlorg.opnsense.unbound_forward
|
||||
acme_challenge:
|
||||
redirect: oxlorg.opnsense.acme_validation
|
||||
acme_automation:
|
||||
redirect: oxlorg.opnsense.acme_action
|
||||
source_nat:
|
||||
redirect: oxlorg.opnsense.nat_source
|
||||
one_to_one:
|
||||
redirect: oxlorg.opnsense.nat_one_to_one
|
||||
wireguard_client:
|
||||
redirect: oxlorg.opnsense.wireguard_peer
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
from os import environ
|
||||
from socket import setdefaulttimeout
|
||||
|
||||
import httpx
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.api import \
|
||||
check_host, ssl_verification, check_response, get_params_path, debug_api, \
|
||||
check_or_load_credentials, api_pretty_exception
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import is_ip6
|
||||
|
||||
DEFAULT_TIMEOUT = 20.0
|
||||
HTTPX_EXCEPTIONS = (
|
||||
httpx.ConnectTimeout, httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout,
|
||||
httpx.TimeoutException, httpx.PoolTimeout,
|
||||
)
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, module: AnsibleModule, timeout: float = DEFAULT_TIMEOUT):
|
||||
self.m = module
|
||||
self.s = self._start(timeout)
|
||||
|
||||
def _start(self, timeout: float) -> httpx.Client:
|
||||
check_host(module=self.m)
|
||||
api_key, api_secret = check_or_load_credentials(module=self.m)
|
||||
if api_secret is None:
|
||||
api_key = self.m.params['api_key']
|
||||
api_secret = self.m.params['api_secret']
|
||||
|
||||
if 'api_timeout' in self.m.params and self.m.params['api_timeout'] is not None:
|
||||
timeout = self.m.params['api_timeout']
|
||||
|
||||
setdefaulttimeout(timeout)
|
||||
|
||||
fw = self.m.params['firewall']
|
||||
if is_ip6(fw, strip_enclosure=False):
|
||||
fw = f"[{fw}]"
|
||||
|
||||
proxy = environ.get('HTTPS_PROXY', None)
|
||||
if proxy is not None and not proxy.startswith('http') and not proxy.startswith('sock'):
|
||||
proxy = None
|
||||
|
||||
return httpx.Client(
|
||||
base_url=f"https://{fw}:{self.m.params['api_port']}/api",
|
||||
auth=(api_key, api_secret),
|
||||
timeout=httpx.Timeout(timeout=timeout, connect=2.0),
|
||||
transport=httpx.HTTPTransport(
|
||||
verify=ssl_verification(module=self.m),
|
||||
retries=self.m.params['api_retries'],
|
||||
proxy=proxy,
|
||||
),
|
||||
headers={'User-Agent': 'Ansible'}
|
||||
)
|
||||
|
||||
def get(self, cnf: dict) -> dict:
|
||||
params_path = get_params_path(cnf=cnf)
|
||||
call_url = f"{cnf['module']}/{cnf['controller']}/{cnf['command']}{params_path}"
|
||||
|
||||
debug_api(
|
||||
module=self.m,
|
||||
method='GET',
|
||||
url=f'{self.s.base_url}{call_url}',
|
||||
)
|
||||
|
||||
try:
|
||||
response = check_response(
|
||||
module=self.m,
|
||||
cnf=cnf,
|
||||
response=self.s.get(url=call_url)
|
||||
)
|
||||
|
||||
except HTTPX_EXCEPTIONS as error:
|
||||
api_pretty_exception(
|
||||
m=self.m, method='GET', error=error,
|
||||
url=f'{self.s.base_url}{call_url}',
|
||||
)
|
||||
raise
|
||||
|
||||
return response
|
||||
|
||||
def post(self, cnf: dict, headers: dict = None) -> dict:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
data = None
|
||||
|
||||
if 'data' in cnf and cnf['data'] is not None and len(cnf['data']) > 0:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
data = cnf['data']
|
||||
|
||||
params_path = get_params_path(cnf=cnf)
|
||||
call_url = f"{cnf['module']}/{cnf['controller']}/{cnf['command']}{params_path}"
|
||||
|
||||
debug_api(
|
||||
module=self.m,
|
||||
method='POST',
|
||||
url=f'{self.s.base_url}{call_url}',
|
||||
data=data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
try:
|
||||
response = check_response(
|
||||
module=self.m,
|
||||
cnf=cnf,
|
||||
response=self.s.post(
|
||||
url=call_url, json=data, headers=headers
|
||||
)
|
||||
)
|
||||
|
||||
except HTTPX_EXCEPTIONS as error:
|
||||
api_pretty_exception(
|
||||
m=self.m, method='POST', error=error,
|
||||
url=f'{self.s.base_url}{call_url}',
|
||||
)
|
||||
raise
|
||||
|
||||
return response
|
||||
|
||||
def close(self) -> None:
|
||||
self.s.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
def single_get(module: AnsibleModule, cnf: dict, timeout: float = DEFAULT_TIMEOUT) -> dict:
|
||||
with Session(module=module, timeout=timeout) as s:
|
||||
response = s.get(cnf=cnf)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def single_post(module: AnsibleModule, cnf: dict, timeout: float = DEFAULT_TIMEOUT, headers: dict = None) -> dict:
|
||||
with Session(module=module, timeout=timeout) as s:
|
||||
response = s.post(cnf=cnf, headers=headers)
|
||||
|
||||
return response
|
||||
|
|
@ -0,0 +1,750 @@
|
|||
# could be implemented using inheritance in the future..
|
||||
|
||||
# NOTE: pylint is basically right, but I really do not want to take the time to refactor this..
|
||||
# pylint: disable=W0212,R0912,R0915
|
||||
|
||||
from typing import Callable
|
||||
from functools import reduce
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
single_get, single_post
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_simple_existing, to_digit, get_matching, simplify_translate, is_unset, \
|
||||
sort_param_lists
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import \
|
||||
exit_bug, ModuleSoftError
|
||||
|
||||
|
||||
class Base:
|
||||
DIFF_FLOAT_ROUND = 1
|
||||
RESP_JOIN_CHAR = ','
|
||||
ATTR_JOIN_CHAR = 'JOIN_CHAR'
|
||||
ATTR_AK_PATH = 'API_KEY_PATH'
|
||||
ATTR_AK_PATH_REQ = 'API_KEY_PATH_REQ' # if a custom path depth is needed
|
||||
ATTR_AK_PATH_GET = 'API_KEY_PATH_GET' # if 'get' needs custom path
|
||||
ATTR_GET_ADD = 'SEARCH_ADDITIONAL' # extract additional data from search-call
|
||||
ATTR_GET_DETAIL_ALL = 'SEARCH_DETAIL_ALL'
|
||||
ATTR_AK_PATH_SPLIT_CHAR = '.'
|
||||
ATTR_BOOL_INVERT = 'FIELDS_BOOL_INVERT'
|
||||
ATTR_TRANSLATE = 'FIELDS_TRANSLATE'
|
||||
ATTR_DIFF_EXCL = 'FIELDS_DIFF_EXCLUDE'
|
||||
ATTR_DIFF_NO_LOG = 'FIELDS_DIFF_NO_LOG'
|
||||
ATTR_VALUE_MAP = 'FIELDS_VALUE_MAPPING'
|
||||
ATTR_VALUE_MAP_RCV = 'FIELDS_VALUE_MAPPING_RCV'
|
||||
ATTR_FIELD_ALL = 'FIELDS_ALL'
|
||||
ATTR_FIELD_CH = 'FIELDS_CHANGE'
|
||||
ATTR_REL_CONT = 'API_CONT_REL'
|
||||
ATTR_REL_CMD = 'API_CMD_REL'
|
||||
ATTR_GET_CONT = 'API_CONT_GET'
|
||||
ATTR_GET_MOD = 'API_MOD_GET'
|
||||
ATTR_API_MOD = 'API_MOD'
|
||||
ATTR_API_CONT = 'API_CONT'
|
||||
ATTR_HEADERS = 'call_headers'
|
||||
ATTR_TYPING = 'FIELDS_TYPING'
|
||||
ATTR_FIELD_ID = 'FIELD_ID' # field we use for matching
|
||||
ATTR_FIELD_PK = 'FIELD_PK' # field opnsense uses as primary key
|
||||
ATTR_CMDS = 'CMDS'
|
||||
PARAM_MATCH_FIELDS = 'match_fields'
|
||||
QUERY_MAX_ENTRIES = 1000
|
||||
VALUE_NO_LOG = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
|
||||
|
||||
REQUIRED_ATTRS = [
|
||||
ATTR_AK_PATH,
|
||||
ATTR_TYPING,
|
||||
ATTR_API_MOD,
|
||||
ATTR_API_CONT,
|
||||
ATTR_FIELD_ALL,
|
||||
ATTR_FIELD_CH,
|
||||
ATTR_CMDS,
|
||||
]
|
||||
|
||||
def __init__(self, instance):
|
||||
self.i = instance # module-specific object
|
||||
self.e = {} # existing entry
|
||||
self.raw = None # save first raw existing entry - to resolve user input per selection
|
||||
|
||||
for attr in self.REQUIRED_ATTRS:
|
||||
if not hasattr(self.i, attr):
|
||||
exit_bug(f"Module has no '{attr}' attribute set!")
|
||||
|
||||
def api_search_post(self, cnf: dict, data: dict = None) -> list:
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
return self._api_post({
|
||||
**cnf,
|
||||
'data': {'current': 1, 'rowCount': self.QUERY_MAX_ENTRIES, **data},
|
||||
})['rows']
|
||||
|
||||
def search(self, match_fields: list = None) -> (dict, list):
|
||||
# workaround if 'get' needs to be performed using other api module/controller
|
||||
cont_get, mod_get = self.i.API_CONT, self.i.API_MOD
|
||||
|
||||
if hasattr(self.i, self.ATTR_GET_CONT):
|
||||
cont_get = getattr(self.i, self.ATTR_GET_CONT)
|
||||
|
||||
if hasattr(self.i, self.ATTR_GET_MOD):
|
||||
mod_get = getattr(self.i, self.ATTR_GET_MOD)
|
||||
|
||||
self.i.call_cnf['controller'] = cont_get
|
||||
self.i.call_cnf['module'] = mod_get
|
||||
|
||||
if self.i.CMDS['search'].startswith('search'):
|
||||
# because of new OPNsense API: https://github.com/O-X-L/ansible-opnsense/issues/51
|
||||
if 'detail' not in self.i.CMDS:
|
||||
exit_bug("To use the 'search' commands you need to also define the related 'detail' (get) command!")
|
||||
|
||||
data = []
|
||||
# if we can - we only perform the 'detail' call for the already matched entry to save on needed requests
|
||||
base_match_fields = False
|
||||
base_match_fields_checked = False
|
||||
force_details = getattr(self.i, self.ATTR_GET_DETAIL_ALL, False)
|
||||
|
||||
for base_entry in self.api_search_post({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['search'],
|
||||
}):
|
||||
if not force_details and match_fields is not None and not base_match_fields_checked:
|
||||
base_match_fields_checked = True
|
||||
base_match_fields = all(field in base_entry for field in match_fields)
|
||||
|
||||
# todo: perform async calls for parallel data fetching
|
||||
detail_entry = {}
|
||||
if force_details or not base_match_fields or \
|
||||
all(base_entry[field] == self.i.p[field] for field in match_fields):
|
||||
detail_entry = self._search_path_handling(
|
||||
self._api_get({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['detail'],
|
||||
'params': [base_entry[self.field_pk]]
|
||||
})
|
||||
)
|
||||
if self.raw is None:
|
||||
self.raw = detail_entry
|
||||
|
||||
data.append({
|
||||
**base_entry,
|
||||
**detail_entry,
|
||||
})
|
||||
|
||||
if self.raw is None:
|
||||
self.raw = self._search_path_handling(
|
||||
self._api_get({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['detail'],
|
||||
})
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
# legacy api handling (fewer requests needed; much simpler client-side handling)
|
||||
data = self._api_get({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['search'],
|
||||
})
|
||||
|
||||
if hasattr(self.i, self.ATTR_GET_ADD):
|
||||
for attr, ak_path in getattr(self.i, self.ATTR_GET_ADD).items():
|
||||
if hasattr(self.i, attr):
|
||||
setattr(
|
||||
self.i, attr,
|
||||
self._search_path_handling(data=data, ak_path=ak_path)
|
||||
)
|
||||
|
||||
return self._search_path_handling(data)
|
||||
|
||||
def _search_path_handling(self, data: dict, ak_path: str = None) -> dict:
|
||||
# resolving API_KEY_PATH's so data from nested dicts gets extracted as configured
|
||||
if ak_path is None:
|
||||
if hasattr(self.i, self.ATTR_AK_PATH_GET):
|
||||
ak_path = getattr(self.i, self.ATTR_AK_PATH_GET)
|
||||
|
||||
elif hasattr(self.i, self.ATTR_AK_PATH):
|
||||
ak_path = getattr(self.i, self.ATTR_AK_PATH)
|
||||
|
||||
try:
|
||||
if ak_path is not None:
|
||||
for k in ak_path.split(self.ATTR_AK_PATH_SPLIT_CHAR):
|
||||
data = data[k]
|
||||
|
||||
return data
|
||||
|
||||
except KeyError:
|
||||
exit_bug(f"Got invalid API_KEY_PATH: '{ak_path}' not matching data '{data}'")
|
||||
|
||||
def get_existing(self, diff_filter: bool = False, details_all: bool = True) -> list:
|
||||
if details_all:
|
||||
# because of new OPNsense API: https://github.com/O-X-L/ansible-opnsense/issues/51
|
||||
# we require all details even if that means we have to perform hundreds of api-calls.. :(
|
||||
setattr(self.i, self.ATTR_GET_DETAIL_ALL, True)
|
||||
|
||||
if diff_filter:
|
||||
# use already existing filtering to get 'clean' int/.. values
|
||||
return get_simple_existing(
|
||||
entries=self._call_search(),
|
||||
simplify_func=self._call_simple(),
|
||||
add_filter=self.build_diff,
|
||||
)
|
||||
|
||||
return get_simple_existing(
|
||||
entries=self._call_search(),
|
||||
simplify_func=self._call_simple(),
|
||||
)
|
||||
|
||||
def find(self, match_fields: list) -> None:
|
||||
if self.i.existing_entries is None:
|
||||
self.i.existing_entries = self._call_search(match_fields)
|
||||
|
||||
match = get_matching(
|
||||
module=self.i.m, existing_items=self.i.existing_entries,
|
||||
compare_item=self.i.p, match_fields=match_fields,
|
||||
simplify_func=self._call_simple(),
|
||||
)
|
||||
|
||||
if match is not None:
|
||||
setattr(self.i, self.i.EXIST_ATTR, match)
|
||||
self.i.exists = True
|
||||
self.i.r['diff']['before'] = self.build_diff(data=match)
|
||||
|
||||
if self.field_pk in match:
|
||||
self.i.call_cnf['params'] = [match[self.field_pk]]
|
||||
|
||||
def process(self) -> None:
|
||||
self.i.call_cnf['controller'] = self.i.API_CONT
|
||||
self.i.call_cnf['module'] = self.i.API_MOD
|
||||
|
||||
if 'state' in self.i.p and self.i.p['state'] == 'absent':
|
||||
if self.i.exists:
|
||||
if hasattr(self.i, 'delete'):
|
||||
self.i.delete()
|
||||
|
||||
else:
|
||||
self.delete()
|
||||
|
||||
else:
|
||||
if 'state' not in self.i.p or self.i.exists:
|
||||
if hasattr(self.i, 'update'):
|
||||
self.i.update()
|
||||
|
||||
else:
|
||||
self.update()
|
||||
|
||||
else:
|
||||
if hasattr(self.i, 'create'):
|
||||
self.i.create()
|
||||
|
||||
else:
|
||||
self.create()
|
||||
|
||||
def create(self) -> dict:
|
||||
self.i.r['changed'] = True
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
return self._api_post({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['add'],
|
||||
'data': self._get_request_data(),
|
||||
})
|
||||
|
||||
def update(self, enable_switch: bool = True) -> dict:
|
||||
self._set_existing()
|
||||
sort_param_lists(self.i.p)
|
||||
sort_param_lists(self.e)
|
||||
|
||||
# checking if changed
|
||||
for field in self.i.FIELDS_CHANGE:
|
||||
if field in self.i.p:
|
||||
if self.PARAM_MATCH_FIELDS in self.i.p:
|
||||
if field in self.i.p[self.PARAM_MATCH_FIELDS]:
|
||||
continue
|
||||
|
||||
if hasattr(self.i, self.ATTR_FIELD_ID):
|
||||
if field == getattr(self.i, self.ATTR_FIELD_ID):
|
||||
continue
|
||||
|
||||
try:
|
||||
if self.i.p[field] is None:
|
||||
self.i.p[field] = ''
|
||||
|
||||
if str(self.e[field]) != str(self.i.p[field]):
|
||||
self.i.r['changed'] = True
|
||||
|
||||
if self.i.p['debug']:
|
||||
self.i.m.warn(
|
||||
f"Field changed: '{field}' "
|
||||
f"'{self.e[field]}' != '{self.i.p[field]}'"
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
except KeyError:
|
||||
exit_bug(
|
||||
f"The field '{field}' seems to be unset - check the modules config!"
|
||||
)
|
||||
|
||||
# update if changed
|
||||
if self.i.r['changed']:
|
||||
if self.i.p['debug']:
|
||||
self.i.m.warn(f"{self.i.r['diff']}")
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
if hasattr(self.i, '_update_call'):
|
||||
response = self.i._update_call()
|
||||
|
||||
else:
|
||||
response = self._api_post({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['set'],
|
||||
'data': self._get_request_data(),
|
||||
})
|
||||
|
||||
if self.i.p['debug']:
|
||||
self.i.m.warn(f"{self.i.r['diff']}")
|
||||
|
||||
return response
|
||||
|
||||
elif enable_switch:
|
||||
self._update_enabled()
|
||||
|
||||
def _update_enabled(self) -> None:
|
||||
existing = getattr(self.i, self.i.EXIST_ATTR)
|
||||
|
||||
if 'enabled' in existing:
|
||||
if existing['enabled'] != self.i.p['enabled']:
|
||||
_bool_invert_fields = []
|
||||
enable = self.i.p['enabled']
|
||||
invert = False
|
||||
|
||||
if hasattr(self.i, self.ATTR_BOOL_INVERT):
|
||||
_bool_invert_fields = getattr(self.i, self.ATTR_BOOL_INVERT)
|
||||
|
||||
if 'enabled' in _bool_invert_fields:
|
||||
invert = True
|
||||
enable = not enable
|
||||
|
||||
if enable:
|
||||
if hasattr(self.i, 'enable'):
|
||||
self.i.enable()
|
||||
|
||||
else:
|
||||
self.enable(invert=invert)
|
||||
|
||||
else:
|
||||
if hasattr(self.i, 'disable'):
|
||||
self.i.disable()
|
||||
|
||||
else:
|
||||
self.disable(invert=invert)
|
||||
|
||||
def delete(self) -> dict:
|
||||
self.i.r['changed'] = True
|
||||
self.i.r['diff']['after'] = {}
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
if hasattr(self.i, '_delete_call'):
|
||||
response = self.i._delete_call()
|
||||
|
||||
else:
|
||||
response = self._api_post({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['del'],
|
||||
})
|
||||
|
||||
if self.i.p['debug']:
|
||||
self.i.m.warn(f"{self.i.r['diff']}")
|
||||
|
||||
return response
|
||||
|
||||
def reload(self) -> dict:
|
||||
# reload the running config
|
||||
cont_rel = self.i.API_CONT
|
||||
cmd_rel = 'reconfigure'
|
||||
|
||||
if hasattr(self.i, self.ATTR_REL_CONT):
|
||||
cont_rel = getattr(self.i, self.ATTR_REL_CONT)
|
||||
|
||||
if hasattr(self.i, self.ATTR_REL_CMD):
|
||||
cmd_rel = getattr(self.i, self.ATTR_REL_CMD)
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
return self._api_post({
|
||||
'module': self.i.API_MOD,
|
||||
'controller': cont_rel,
|
||||
'command': cmd_rel,
|
||||
'params': []
|
||||
})
|
||||
|
||||
def _get_request_data(self) -> dict:
|
||||
if hasattr(self.i, '_build_request'):
|
||||
return self.i._build_request()
|
||||
|
||||
return self.build_request()
|
||||
|
||||
def _change_enabled_state(self) -> dict:
|
||||
return self._api_post({
|
||||
**self.i.call_cnf,
|
||||
'command': self.i.CMDS['toggle'],
|
||||
'params': [getattr(self.i, self.i.EXIST_ATTR)[self.field_pk]],
|
||||
})
|
||||
|
||||
def is_enabled(self, invert: bool = False) -> bool:
|
||||
is_enabled = getattr(self.i, self.i.EXIST_ATTR)['enabled']
|
||||
|
||||
if invert:
|
||||
is_enabled = not is_enabled
|
||||
|
||||
return is_enabled
|
||||
|
||||
def enable(self, invert: bool = False) -> dict:
|
||||
if self.i.exists and not self.is_enabled(invert=invert):
|
||||
self.i.r['changed'] = True
|
||||
if not invert:
|
||||
self.i.r['diff']['before'] = {'enabled': False}
|
||||
self.i.r['diff']['after'] = {'enabled': True}
|
||||
|
||||
else:
|
||||
self.i.r['diff']['before'] = {'enabled': True}
|
||||
self.i.r['diff']['after'] = {'enabled': False}
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
return self._change_enabled_state()
|
||||
|
||||
def disable(self, invert: bool = False) -> dict:
|
||||
if self.i.exists and self.is_enabled(invert=invert):
|
||||
self.i.r['changed'] = True
|
||||
if not invert:
|
||||
self.i.r['diff']['before'] = {'enabled': True}
|
||||
self.i.r['diff']['after'] = {'enabled': False}
|
||||
|
||||
else:
|
||||
self.i.r['diff']['before'] = {'enabled': False}
|
||||
self.i.r['diff']['after'] = {'enabled': True}
|
||||
|
||||
if not self.i.m.check_mode:
|
||||
return self._change_enabled_state()
|
||||
|
||||
def build_diff(self, data: dict) -> dict:
|
||||
if not isinstance(data, dict):
|
||||
exit_bug('The diff-source object must be of type dict!')
|
||||
|
||||
_exclude_fields = []
|
||||
_no_log_fields = []
|
||||
|
||||
if hasattr(self.i, self.ATTR_DIFF_EXCL):
|
||||
_exclude_fields = getattr(self.i, self.ATTR_DIFF_EXCL)
|
||||
|
||||
if hasattr(self.i, self.ATTR_DIFF_NO_LOG):
|
||||
_no_log_fields = getattr(self.i, self.ATTR_DIFF_NO_LOG)
|
||||
|
||||
self._set_existing()
|
||||
|
||||
diff = {
|
||||
self.field_pk: self.e[self.field_pk] if self.field_pk in self.e else None
|
||||
}
|
||||
|
||||
for field in self.i.FIELDS_ALL:
|
||||
if field in _exclude_fields:
|
||||
continue
|
||||
|
||||
if field in _no_log_fields:
|
||||
diff[field] = self.VALUE_NO_LOG
|
||||
continue
|
||||
|
||||
stringify = True
|
||||
|
||||
try:
|
||||
diff[field] = data[field]
|
||||
|
||||
except KeyError:
|
||||
if field in self.i.p:
|
||||
diff[field] = self.i.p[field]
|
||||
|
||||
if isinstance(diff[field], list):
|
||||
try:
|
||||
diff[field].sort()
|
||||
|
||||
except TypeError:
|
||||
raise exit_bug(f"Field not defined as 'select_opt_list' type: {diff[field]}")
|
||||
|
||||
stringify = False
|
||||
|
||||
elif isinstance(diff[field], str) and diff[field].isnumeric:
|
||||
try:
|
||||
diff[field] = int(diff[field])
|
||||
stringify = False
|
||||
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
elif isinstance(diff[field], dict) and self.field_pk in diff[field]:
|
||||
diff[field] = diff[field][self.field_pk]
|
||||
|
||||
elif isinstance(diff[field], (bool, int)):
|
||||
stringify = False
|
||||
|
||||
elif diff[field] is None:
|
||||
diff[field] = ''
|
||||
|
||||
if stringify:
|
||||
try:
|
||||
diff[field] = round(float(diff[field]), self.DIFF_FLOAT_ROUND)
|
||||
stringify = False
|
||||
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
if stringify:
|
||||
diff[field] = str(diff[field])
|
||||
|
||||
return diff
|
||||
|
||||
def build_request(self, ignore_fields: list = None) -> dict:
|
||||
request = {}
|
||||
_translate_fields = {}
|
||||
_translate_values = {}
|
||||
_bool_invert_fields = []
|
||||
|
||||
if ignore_fields is None:
|
||||
ignore_fields = []
|
||||
|
||||
if is_unset(self.e):
|
||||
self.e = getattr(self.i, self.i.EXIST_ATTR)
|
||||
|
||||
if hasattr(self.i, self.ATTR_TRANSLATE):
|
||||
_translate_fields = getattr(self.i, self.ATTR_TRANSLATE)
|
||||
|
||||
if hasattr(self.i, self.ATTR_VALUE_MAP):
|
||||
_translate_values = getattr(self.i, self.ATTR_VALUE_MAP)
|
||||
|
||||
if hasattr(self.i, self.ATTR_BOOL_INVERT):
|
||||
_bool_invert_fields = getattr(self.i, self.ATTR_BOOL_INVERT)
|
||||
|
||||
for field in self.i.FIELDS_ALL:
|
||||
if field in ignore_fields:
|
||||
continue
|
||||
|
||||
opn_field = field
|
||||
if field in _translate_fields:
|
||||
opn_field = _translate_fields[field]
|
||||
|
||||
if field in self.i.p:
|
||||
opn_data = self.i.p[field]
|
||||
|
||||
elif field in self.e:
|
||||
opn_data = self.e[field]
|
||||
|
||||
else:
|
||||
opn_data = ''
|
||||
|
||||
if field in _translate_values:
|
||||
try:
|
||||
opn_data = _translate_values[field][opn_data]
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if isinstance(opn_data, bool):
|
||||
if field in _bool_invert_fields:
|
||||
opn_data = not opn_data
|
||||
|
||||
request[opn_field] = to_digit(opn_data)
|
||||
|
||||
elif isinstance(opn_data, list):
|
||||
join_char = self.RESP_JOIN_CHAR
|
||||
|
||||
if hasattr(self.i, self.ATTR_JOIN_CHAR):
|
||||
join_char = getattr(self.i, self.ATTR_JOIN_CHAR)
|
||||
|
||||
request[opn_field] = join_char.join(opn_data)
|
||||
|
||||
elif opn_data is None:
|
||||
request[opn_field] = ''
|
||||
|
||||
else:
|
||||
request[opn_field] = opn_data
|
||||
|
||||
if isinstance(opn_field, tuple):
|
||||
hreqest = reduce(lambda r, i: r.setdefault(i, {}), opn_field[:-1], request)
|
||||
hreqest[opn_field[-1]] = request.pop(opn_field)
|
||||
|
||||
payload = request
|
||||
|
||||
if hasattr(self.i, self.ATTR_AK_PATH_REQ):
|
||||
ak_path = getattr(self.i, self.ATTR_AK_PATH_REQ).split(self.ATTR_AK_PATH_SPLIT_CHAR)
|
||||
ak_path.reverse()
|
||||
|
||||
for k in ak_path:
|
||||
payload = {k: payload}
|
||||
|
||||
elif hasattr(self.i, self.ATTR_AK_PATH):
|
||||
# request only needs the last key
|
||||
ak_path = getattr(self.i, self.ATTR_AK_PATH)
|
||||
attr_ak = ak_path
|
||||
|
||||
if ak_path.find('.') != -1:
|
||||
attr_ak = ak_path.rsplit(self.ATTR_AK_PATH_SPLIT_CHAR, 1)[1]
|
||||
|
||||
payload = {attr_ak: payload}
|
||||
|
||||
return payload
|
||||
|
||||
def find_single_link(
|
||||
self, field: str, existing: dict, set_field: str = None, existing_field_id: str = 'name',
|
||||
fail: bool = True
|
||||
) -> bool:
|
||||
entry = None
|
||||
|
||||
if not is_unset(self.i.p[field]):
|
||||
found = False
|
||||
if set_field is None:
|
||||
set_field = field
|
||||
|
||||
if len(existing) > 0:
|
||||
for uuid, item in existing.items():
|
||||
if item[existing_field_id] == self.i.p[field]:
|
||||
self.i.p[set_field] = uuid
|
||||
entry = item[existing_field_id]
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
if fail:
|
||||
if self.i.p['debug']:
|
||||
self.i.m.warn(f"Unable to find link by field '{field}': '{self.i.p[field]}' in '{existing}'")
|
||||
|
||||
self.i.m.fail_json(
|
||||
f"Provided {field} '{self.i.p[field]}' was not found!"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
if 'before' in self.i.r['diff'] and set_field in self.i.r['diff']['before']:
|
||||
self.i.r['diff']['before'][set_field] = entry
|
||||
|
||||
return True
|
||||
|
||||
def find_multiple_links(
|
||||
self, field: str, existing: dict, set_field: str = None, existing_field_id: str = 'name',
|
||||
fail: bool = True, fail_soft: bool = False
|
||||
) -> bool:
|
||||
provided = len(self.i.p[field]) > 0
|
||||
uuids = []
|
||||
entries = []
|
||||
|
||||
if not provided:
|
||||
return True
|
||||
|
||||
if existing is not None and len(existing) > 0:
|
||||
for uuid, item in existing.items():
|
||||
if item[existing_field_id] in self.i.p[field]:
|
||||
uuids.append(uuid)
|
||||
entries.append(item[existing_field_id])
|
||||
|
||||
if len(uuids) == len(self.i.p[field]):
|
||||
break
|
||||
|
||||
if len(uuids) != len(self.i.p[field]):
|
||||
msg = f"At least one of the provided {field} entries was not found!"
|
||||
|
||||
if fail:
|
||||
self.i.m.fail_json(msg)
|
||||
|
||||
if fail_soft:
|
||||
raise ModuleSoftError(msg)
|
||||
|
||||
return False
|
||||
|
||||
if set_field is None:
|
||||
set_field = field
|
||||
|
||||
self.i.p[set_field] = uuids
|
||||
if set_field in self.i.r['diff']['before']:
|
||||
entries.sort()
|
||||
self.i.r['diff']['before'][set_field] = entries
|
||||
|
||||
if set_field in self.i.r['diff']['after']:
|
||||
self.i.r['diff']['after'][set_field].sort()
|
||||
|
||||
return True
|
||||
|
||||
def _set_existing(self) -> None:
|
||||
if is_unset(self.e):
|
||||
_existing = getattr(self.i, self.i.EXIST_ATTR)
|
||||
|
||||
if _existing is not None and len(_existing) > 0:
|
||||
self.e = _existing
|
||||
|
||||
def simplify_existing(self, existing: dict) -> dict:
|
||||
translate, typing, bool_invert, value_map = {}, {}, [], {}
|
||||
|
||||
if hasattr(self.i, self.ATTR_TRANSLATE):
|
||||
translate = getattr(self.i, self.ATTR_TRANSLATE)
|
||||
|
||||
if hasattr(self.i, self.ATTR_TYPING):
|
||||
typing = getattr(self.i, self.ATTR_TYPING)
|
||||
|
||||
if hasattr(self.i, self.ATTR_BOOL_INVERT):
|
||||
bool_invert = getattr(self.i, self.ATTR_BOOL_INVERT)
|
||||
|
||||
if hasattr(self.i, self.ATTR_VALUE_MAP_RCV):
|
||||
value_map = getattr(self.i, self.ATTR_VALUE_MAP_RCV)
|
||||
|
||||
elif hasattr(self.i, self.ATTR_VALUE_MAP):
|
||||
value_map = getattr(self.i, self.ATTR_VALUE_MAP)
|
||||
|
||||
return simplify_translate(
|
||||
existing=existing,
|
||||
typing=typing,
|
||||
translate=translate,
|
||||
bool_invert=bool_invert,
|
||||
value_map=value_map,
|
||||
)
|
||||
|
||||
@property
|
||||
def field_pk(self) -> str:
|
||||
if hasattr(self.i, self.ATTR_FIELD_PK):
|
||||
return getattr(self.i, self.ATTR_FIELD_PK)
|
||||
|
||||
return 'uuid'
|
||||
|
||||
def _call_simple(self) -> Callable:
|
||||
if hasattr(self.i, 'simplify_existing'):
|
||||
return self.i.simplify_existing
|
||||
|
||||
if hasattr(self.i, '_simplify_existing'):
|
||||
return self.i._simplify_existing
|
||||
|
||||
return self.simplify_existing
|
||||
|
||||
def _call_search(self, match_fields: list = None) -> (list, dict):
|
||||
if hasattr(self.i, '_search_call'):
|
||||
return self.i._search_call()
|
||||
|
||||
return self.search(match_fields)
|
||||
|
||||
def _api_headers(self) -> dict:
|
||||
if hasattr(self.i, self.ATTR_HEADERS):
|
||||
return getattr(self.i, self.ATTR_HEADERS)
|
||||
|
||||
return {}
|
||||
|
||||
def _api_post(self, cnf: dict) -> (dict, list):
|
||||
if hasattr(self.i, 's'):
|
||||
return self.i.s.post(
|
||||
cnf=cnf,
|
||||
headers=self._api_headers()
|
||||
)
|
||||
|
||||
return single_post(
|
||||
cnf=cnf,
|
||||
module=self.i.m,
|
||||
headers=self._api_headers()
|
||||
)
|
||||
|
||||
def _api_get(self, cnf: dict) -> (dict, list):
|
||||
if hasattr(self.i, 's'):
|
||||
return self.i.s.get(cnf=cnf)
|
||||
|
||||
return single_get(
|
||||
cnf=cnf,
|
||||
module=self.i.m
|
||||
)
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.base import Base
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
validate_int_fields, validate_str_fields
|
||||
|
||||
|
||||
class BaseShared:
|
||||
def __init__(self, m: AnsibleModule, r: dict, s: Session):
|
||||
if hasattr(self, 'TIMEOUT'):
|
||||
self.s = Session(
|
||||
module=m,
|
||||
timeout=self.TIMEOUT,
|
||||
) if s is None else s
|
||||
|
||||
else:
|
||||
self.s = Session(module=m) if s is None else s
|
||||
|
||||
self.m = m
|
||||
self.p = m.params
|
||||
self.r = r
|
||||
self.b = Base(instance=self)
|
||||
self.exists = False
|
||||
self.existing_entries = None
|
||||
self.call_cnf = {
|
||||
'module': self.b.i.API_MOD,
|
||||
'controller': self.b.i.API_CONT,
|
||||
}
|
||||
|
||||
def _check_validators(self):
|
||||
if 'state' in self.p and self.p['state'] != 'present':
|
||||
return
|
||||
|
||||
if hasattr(self.b.i, 'STR_VALIDATIONS'):
|
||||
if hasattr(self.b.i, 'STR_LEN_VALIDATIONS'):
|
||||
validate_str_fields(
|
||||
module=self.m,
|
||||
data=self.p,
|
||||
field_regex=self.b.i.STR_VALIDATIONS,
|
||||
field_minmax_length=self.b.i.STR_LEN_VALIDATIONS
|
||||
)
|
||||
|
||||
else:
|
||||
validate_str_fields(module=self.m, data=self.p, field_regex=self.b.i.STR_VALIDATIONS)
|
||||
|
||||
elif hasattr(self.b.i, 'STR_LEN_VALIDATIONS'):
|
||||
validate_str_fields(module=self.m, data=self.p, field_minmax_length=self.b.i.STR_LEN_VALIDATIONS)
|
||||
|
||||
if hasattr(self.b.i, 'INT_VALIDATIONS'):
|
||||
validate_int_fields(module=self.m, data=self.p, field_minmax=self.b.i.INT_VALIDATIONS)
|
||||
|
||||
|
||||
class BaseModule(BaseShared):
|
||||
def __init__(self, m: AnsibleModule, r: dict, s: Session = None, f: dict = None, multi: dict = None):
|
||||
super().__init__(m, r, s)
|
||||
if f is None:
|
||||
f = {}
|
||||
|
||||
if multi is not None and len(multi) > 0:
|
||||
# override params by MultiModule
|
||||
self.p = multi
|
||||
|
||||
self.fail_verify = f.get('verify', False)
|
||||
self.fail_process = f.get('process', False)
|
||||
|
||||
def _base_check(self, match_fields: list = None):
|
||||
self._check_validators()
|
||||
|
||||
if match_fields is None:
|
||||
if 'match_fields' in self.p:
|
||||
match_fields = self.p['match_fields']
|
||||
|
||||
elif hasattr(self, 'FIELD_ID'):
|
||||
match_fields = [self.FIELD_ID]
|
||||
|
||||
if match_fields is not None:
|
||||
self.b.find(match_fields=match_fields)
|
||||
if self.exists:
|
||||
self.call_cnf['params'] = [getattr(self, self.EXIST_ATTR)[self.b.field_pk]]
|
||||
|
||||
if 'state' not in self.p or self.p['state'] == 'present':
|
||||
self.r['diff']['after'] = self.b.build_diff(data=self.p)
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
def get_existing(self) -> list:
|
||||
return self.b.get_existing()
|
||||
|
||||
def process(self) -> None:
|
||||
self.b.process()
|
||||
|
||||
def create(self) -> None:
|
||||
self.b.create()
|
||||
|
||||
def update(self) -> None:
|
||||
self.b.update()
|
||||
|
||||
def delete(self) -> None:
|
||||
self.b.delete()
|
||||
|
||||
def reload(self) -> None:
|
||||
self.b.reload()
|
||||
|
||||
|
||||
class GeneralModule(BaseShared):
|
||||
# has only a single entry; cannot be deleted or created
|
||||
EXIST_ATTR = 'settings'
|
||||
|
||||
def __init__(self, m: AnsibleModule, r: dict, s: Session = None):
|
||||
super().__init__(m, r, s)
|
||||
self.settings = {}
|
||||
|
||||
def _base_check(self):
|
||||
self._check_validators()
|
||||
self.settings = self._search_call()
|
||||
self._build_diff()
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
def _search_call(self) -> dict:
|
||||
return self.b.simplify_existing(self.b.search())
|
||||
|
||||
def get_existing(self) -> dict:
|
||||
return self._search_call()
|
||||
|
||||
def process(self) -> None:
|
||||
self.update()
|
||||
|
||||
def update(self) -> None:
|
||||
self.b.update(enable_switch=False)
|
||||
|
||||
def reload(self) -> None:
|
||||
self.b.reload()
|
||||
|
||||
def _build_diff(self) -> None:
|
||||
self.r['diff']['before'] = self.b.build_diff(self.settings)
|
||||
self.r['diff']['after'] = self.b.build_diff({
|
||||
k: v for k, v in self.p.items() if k in self.settings
|
||||
})
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
try:
|
||||
from ansible.module_utils.common.errors import AnsibleModuleError
|
||||
|
||||
except ModuleNotFoundError:
|
||||
class AnsibleModuleError(Exception):
|
||||
pass
|
||||
|
||||
MODULE_EXCEPTIONS = (ModuleNotFoundError, ImportError)
|
||||
|
||||
|
||||
class ModuleSoftError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ModuleValidationError(ModuleSoftError):
|
||||
pass
|
||||
|
||||
|
||||
def exit_bug(msg: str):
|
||||
raise AnsibleModuleError(f"THIS MIGHT BE A MODULE-BUG: {msg}")
|
||||
|
||||
|
||||
def exit_debug(msg: str):
|
||||
raise AnsibleModuleError(f"DEBUG INFO: {msg}")
|
||||
|
||||
|
||||
def exit_env(msg: str):
|
||||
raise AnsibleModuleError(f"ENVIRONMENTAL ERROR: {msg}")
|
||||
|
||||
|
||||
def exit_cnf(msg: str):
|
||||
raise AnsibleModuleError(f"CONFIG ERROR: {msg}")
|
||||
|
||||
|
||||
def module_dependency_error() -> None:
|
||||
exit_env(
|
||||
'For this Ansible-module to work you must install its dependencies first: '
|
||||
"'python3 -m pip install --upgrade httpx'"
|
||||
)
|
||||
|
|
@ -0,0 +1,596 @@
|
|||
from abc import ABC
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.arg_spec import ModuleArgumentSpecValidator
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import ModuleSoftError
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import diff_remove_empty
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
OPN_MOD_ARGS, RELOAD_MOD_ARG_DEF_FALSE
|
||||
|
||||
|
||||
def build_multi_mod_args(
|
||||
mod_args: dict,
|
||||
aliases: list = None,
|
||||
description: str = None,
|
||||
not_required: list[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Function to dynamically build the module-arguments required for mass-management.
|
||||
|
||||
:param mod_args: The module-specific arguments
|
||||
:param aliases: List of module-specific aliases for the 'multi' argument
|
||||
:param description: An optional description for the 'multi' argument
|
||||
:param not_required: List of module-specific arguments (only keys) that should be set to be NOT required
|
||||
:return: Module arguments required for mass-management
|
||||
"""
|
||||
args_base = mod_args.copy()
|
||||
|
||||
aliases_purge = ['multi_delete', 'purge', 'many_purge']
|
||||
if aliases is None:
|
||||
aliases = []
|
||||
|
||||
else:
|
||||
for a in aliases:
|
||||
aliases_purge.append(f'{a}_purge')
|
||||
|
||||
aliases.append('many')
|
||||
|
||||
if description is None:
|
||||
description = 'Provide multiple entries to manage'
|
||||
|
||||
for opn_arg in OPN_MOD_ARGS:
|
||||
if opn_arg in args_base:
|
||||
args_base.pop(opn_arg)
|
||||
|
||||
if 'reload' in args_base:
|
||||
args_base.pop('reload')
|
||||
|
||||
opn_args_multi = OPN_MOD_ARGS.copy()
|
||||
opn_args_multi['firewall']['required'] = False
|
||||
|
||||
if not_required is None:
|
||||
not_required = []
|
||||
|
||||
not_required.append('match_fields')
|
||||
for field in not_required:
|
||||
if field in args_base:
|
||||
args_base[field]['required'] = False
|
||||
|
||||
entry_args_full = {**args_base, **RELOAD_MOD_ARG_DEF_FALSE, **opn_args_multi}
|
||||
|
||||
return dict(
|
||||
multi=dict(
|
||||
type='list', required=False, default=[], aliases=aliases,
|
||||
description=description,
|
||||
elements='dict', options=entry_args_full,
|
||||
),
|
||||
multi_purge=dict(
|
||||
type='list', required=False, default=[], aliases=aliases_purge,
|
||||
description='Provide multiple entries to purge (delete or disable)',
|
||||
elements='dict', options=entry_args_full,
|
||||
),
|
||||
multi_control=dict(
|
||||
type='dict', required=False, default={}, aliases=['multi_ctrl', 'mc'],
|
||||
description=description,
|
||||
options=dict(
|
||||
# NOTE: we keep state and enabled outside overrides for convenience
|
||||
state=dict(type='str', required=False, choices=['present', 'absent'], default=None),
|
||||
enabled=dict(type='bool', required=False, default=None),
|
||||
# overrides for other parameters
|
||||
override=dict(
|
||||
type='dict', required=False, default={}, description='Parameters to override for all entries',
|
||||
aliases=['all', 'overrides'],
|
||||
),
|
||||
fail_verify=dict(
|
||||
type='bool', required=False, default=False, aliases=['fail_verification'],
|
||||
description='Fail module if a single entry fails the verification.'
|
||||
),
|
||||
fail_process=dict(
|
||||
type='bool', required=False, default=True, aliases=['fail_proc', 'fail_processing'],
|
||||
description='Fail module if a single entry fails to be processed.'
|
||||
),
|
||||
output_info=dict(type='bool', required=False, default=False, aliases=['info']),
|
||||
purge_action=dict(
|
||||
type='str', required=False, default='delete', choices=['disable', 'delete'],
|
||||
description='What action to perform on the entries matched by the purge'
|
||||
),
|
||||
purge_filter=dict(
|
||||
type='dict', required=False, default={}, aliases=['purge_filters'],
|
||||
description='Field-value pairs to filter on - per example: {param1: test} '
|
||||
"- to only purge entries that have 'param1' set to 'test'. WARNING: Make sure to run a "
|
||||
'check-mode beforehand and manually verify the deletions!'
|
||||
),
|
||||
purge_filter_invert=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If true - it will purge all but the filtered ones. WARNING: Make sure to run a '
|
||||
'check-mode beforehand and manually verify the deletions!'
|
||||
),
|
||||
purge_filter_partial=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If true - the filter will also match if it is just a partial value-match. WARNING: '
|
||||
'Make sure to run a check-mode beforehand and manually verify the deletions!'
|
||||
),
|
||||
purge_all=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If set to true and neither entries, nor filters are provided - all entries will be '
|
||||
'purged. WARNING: Make sure to run a check-mode beforehand and manually '
|
||||
'verify the deletions!'
|
||||
),
|
||||
purge_unconfigured=dict(
|
||||
type='bool', required=False, default=False, aliases=['purge_unknown', 'purge_orphaned'],
|
||||
description='Usable if configured entries are supplied - will delete all entries NOT matched with '
|
||||
'the configured ones. WARNING: Make sure to run a check-mode beforehand and manually '
|
||||
'verify the deletions!'
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MultiModuleCallbacks(ABC):
|
||||
# pylint: disable=W0613
|
||||
@staticmethod
|
||||
def build(entry: dict) -> dict:
|
||||
"""
|
||||
Callback to make modifications to a raw entry that was provided by the user, before it gets processed.
|
||||
This happens after the overrides were applied.
|
||||
|
||||
:param entry: The entry that can be modified
|
||||
:return: The modified entry
|
||||
"""
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def validation(entry: dict) -> bool:
|
||||
"""
|
||||
Callback to validate a raw entry that was provided by the use.
|
||||
This Validation extends the default Ansible-Module-Argument Validation.
|
||||
It is called after the 'build' callback.
|
||||
|
||||
:param entry: The entry that should be validated
|
||||
:return: If the entry was valid
|
||||
"""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_existing(meta_entry: BaseModule) -> dict:
|
||||
"""
|
||||
Callback to pull the 'existing_entries' that will be written to the cache.
|
||||
|
||||
:param meta_entry: A dummy/meta-entry as BaseModule-instance that
|
||||
is used to pull all existing entries from the API
|
||||
:return: Result that will be used as cache. The key 'main' is required to contain the primary entries processed
|
||||
"""
|
||||
return {'main': meta_entry.get_existing()}
|
||||
|
||||
@staticmethod
|
||||
def set_existing(entry: BaseModule, cache: dict):
|
||||
"""
|
||||
Callback to set the 'existing_entries' of an entry-instance that is about to be processed
|
||||
|
||||
:param entry: The entry BaseModule-instance that might need 'existing_entries' to be set
|
||||
:param cache: The full cache
|
||||
:return: None
|
||||
"""
|
||||
entry.existing_entries = cache['main']
|
||||
|
||||
@staticmethod
|
||||
def update_existing(entry: BaseModule, cache: dict) -> dict:
|
||||
"""
|
||||
Callback to update the cache after an entry was processed
|
||||
|
||||
:param entry: The entry that was just processed
|
||||
:param cache: The full cache
|
||||
:return: The updated cache
|
||||
"""
|
||||
|
||||
# adding the config of the new entry to the cache (WITHOUT UUID!)
|
||||
if not entry.exists and entry.p.get('state', 'present') == 'present': # was just created
|
||||
entry_cnf = entry.p.copy()
|
||||
for opn_arg in OPN_MOD_ARGS:
|
||||
try:
|
||||
entry_cnf.pop(opn_arg)
|
||||
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
cache['main'].append(entry_cnf)
|
||||
|
||||
return cache
|
||||
|
||||
@staticmethod
|
||||
def purge_exclude(entry: dict) -> bool:
|
||||
"""
|
||||
Callback to check if an entry should be excluded from purge/deletion.
|
||||
This should only be used if there are built-in entries that should be protected.
|
||||
|
||||
:param entry: The entry that should get purged/deleted
|
||||
:return: If the entry should be excluded from being purged
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
# pylint: disable=R0902,R0913,R0915,R0917
|
||||
class MultiModule:
|
||||
def __init__(
|
||||
self, module: AnsibleModule, result: dict, entry_args: dict, kind: str, obj: BaseModule,
|
||||
validation: bool = True, cache_existing: bool = True,
|
||||
callbacks: MultiModuleCallbacks = None,
|
||||
):
|
||||
self.m = module
|
||||
self.p = module.params
|
||||
self.mc = self.p['multi_control']
|
||||
self.r = result
|
||||
self.o = obj
|
||||
self.k = kind
|
||||
|
||||
if hasattr(self.o, 'TIMEOUT'):
|
||||
self.s = Session(
|
||||
module=self.m,
|
||||
timeout=getattr(self.o, 'TIMEOUT'),
|
||||
)
|
||||
|
||||
else:
|
||||
self.s = Session(module=self.m)
|
||||
|
||||
self.meta_entry = self.o(module=self.m, session=self.s, result={})
|
||||
self.mod_entry_args = entry_args
|
||||
self.validation = validation
|
||||
self.cache = {}
|
||||
self._cache_original = {}
|
||||
self.cache_existing = cache_existing
|
||||
self.callbacks: MultiModuleCallbacks = callbacks
|
||||
if self.callbacks is None:
|
||||
self.callbacks = MultiModuleCallbacks()
|
||||
|
||||
self._init_match_fields()
|
||||
self._init_cases()
|
||||
|
||||
def _init_match_fields(self):
|
||||
self.field_id = None
|
||||
|
||||
if hasattr(self.o, 'FIELD_ID'):
|
||||
self.field_id = getattr(self.o, 'FIELD_ID')
|
||||
|
||||
self.match_fields = self.p.get('match_fields', [self.field_id])
|
||||
|
||||
if self.field_id is None:
|
||||
fallback = None
|
||||
if 'match_fields' in self.p and self.p['match_fields'] is not None:
|
||||
if len(self.p['match_fields']) > 0 and not isinstance(self.p['match_fields'], list):
|
||||
self.p['match_fields'] = [self.p['match_fields']]
|
||||
|
||||
fallback = self.p['match_fields'][0]
|
||||
|
||||
self.field_id = getattr(self.o, 'MULTI_DIFF_KEY', fallback)
|
||||
|
||||
def _init_cases(self):
|
||||
self._has_multi_crud_entries = len(self.p['multi']) > 0
|
||||
self._has_multi_purge_entries = len(self.p['multi_purge']) > 0
|
||||
self._has_multi_purge_filters = len(self.mc['purge_filter']) > 0
|
||||
|
||||
def _is_multi_purge(self) -> bool:
|
||||
return self._has_multi_purge_entries or self.mc['purge_all'] or self._has_multi_purge_filters
|
||||
|
||||
def _is_multi_crud(self) -> bool:
|
||||
return self._has_multi_crud_entries
|
||||
|
||||
def process(self) -> None:
|
||||
if self.cache_existing:
|
||||
self.cache = self.callbacks.get_existing(self.meta_entry)
|
||||
self._cache_original = self.cache.copy()
|
||||
|
||||
if self._is_multi_purge():
|
||||
self._purge()
|
||||
|
||||
elif self._is_multi_crud():
|
||||
self._create_update()
|
||||
self._purge_unconfigured()
|
||||
|
||||
else:
|
||||
self.m.fail_json('Got invalid Mass-Management arguments!')
|
||||
|
||||
if self.r['changed'] and self.p['reload']:
|
||||
self.meta_entry.reload()
|
||||
|
||||
self.s.close()
|
||||
|
||||
# CREATE/UPDATE METHODS
|
||||
def _validate_entry(self, entry: dict) -> bool:
|
||||
result = False
|
||||
if self.mc['fail_verify']:
|
||||
error_func = self.m.fail_json
|
||||
|
||||
else:
|
||||
error_func = self.m.warn
|
||||
|
||||
validation = ModuleArgumentSpecValidator(self.mod_entry_args).validate(parameters=entry)
|
||||
|
||||
try:
|
||||
validation_error = validation.errors[0]
|
||||
|
||||
except IndexError:
|
||||
validation_error = None
|
||||
|
||||
if validation_error:
|
||||
error_func(f"Got invalid config for {self.k} '{self._entry_id(entry)}': {validation_error}")
|
||||
|
||||
else:
|
||||
result = True
|
||||
|
||||
if result:
|
||||
try:
|
||||
result = self.callbacks.validation(entry)
|
||||
|
||||
except ModuleSoftError as e:
|
||||
result = False
|
||||
error_func(e)
|
||||
|
||||
return result
|
||||
|
||||
def _build_entries(self) -> list:
|
||||
overrides = {
|
||||
'reload': False,
|
||||
**self.mc['override'], # user-defined overrides
|
||||
}
|
||||
if 'match_fields' in self.p:
|
||||
overrides['match_fields'] = self.p['match_fields']
|
||||
|
||||
if self.mc['state'] is not None:
|
||||
overrides['state'] = self.mc['state']
|
||||
|
||||
if self.mc['enabled'] is not None:
|
||||
overrides['enabled'] = self.mc['enabled']
|
||||
|
||||
# build list of valid entries or fail if invalid config is not permitted
|
||||
valid_entries = []
|
||||
for entry_cnf in self.p['multi']:
|
||||
# apply overrides
|
||||
entry = {
|
||||
**entry_cnf,
|
||||
**overrides,
|
||||
}
|
||||
|
||||
for f in ['debug', 'profiling']:
|
||||
if f not in entry:
|
||||
entry[f] = self.p[f]
|
||||
|
||||
entry = self.callbacks.build(entry)
|
||||
|
||||
if not self.validation:
|
||||
valid_entries.append(entry)
|
||||
|
||||
else:
|
||||
# (re-)validate the entry like ansible does on module-init
|
||||
if entry['debug']:
|
||||
self.m.warn(f"Validating {self.k}: '{entry}'")
|
||||
|
||||
if self._validate_entry(entry):
|
||||
valid_entries.append(entry)
|
||||
|
||||
return valid_entries
|
||||
|
||||
def _create_update(self):
|
||||
if not self._has_multi_crud_entries:
|
||||
return
|
||||
|
||||
for entry_cnf in self._build_entries():
|
||||
# process single entry like in the single-module
|
||||
entry_result = dict(
|
||||
changed=False,
|
||||
diff={
|
||||
'before': {},
|
||||
'after': {},
|
||||
}
|
||||
)
|
||||
|
||||
self.p['debug'] = entry_cnf['debug'] # per entry switch
|
||||
|
||||
if self.p['debug'] or self.mc['output_info']:
|
||||
self.m.warn(f"Processing {self.k}: '{self._entry_id(entry_cnf)}'")
|
||||
|
||||
try:
|
||||
entry = self._init_entry(entry_cnf, entry_result)
|
||||
try:
|
||||
entry.check()
|
||||
|
||||
except KeyError as e:
|
||||
if str(e) == "KeyError: 'uuid'":
|
||||
raise ModuleSoftError("Cannot modify entry that was just created!")
|
||||
|
||||
entry.process()
|
||||
|
||||
if self.cache_existing:
|
||||
self.cache = self.callbacks.update_existing(entry, cache=self.cache)
|
||||
|
||||
self._add_entry_result(entry, entry_result)
|
||||
|
||||
except ModuleSoftError:
|
||||
continue
|
||||
|
||||
# PURGE METHODS
|
||||
def _purge_entry(self, entry_cnf: dict) -> None:
|
||||
entry_name = self._entry_id(entry_cnf)
|
||||
|
||||
try:
|
||||
entry_result = dict(
|
||||
changed=False,
|
||||
diff={
|
||||
'before': {},
|
||||
'after': {},
|
||||
}
|
||||
)
|
||||
entry = self._init_entry(entry_cnf, entry_result)
|
||||
|
||||
if not entry.p.get('debug', False):
|
||||
entry.p['debug'] = self.p['debug']
|
||||
|
||||
entry.p['state'] = 'absent' if self.mc['purge_action'] == 'delete' else 'present'
|
||||
entry_cnf['match_fields'] = self.match_fields
|
||||
|
||||
entry.check()
|
||||
if not entry.exists:
|
||||
return
|
||||
|
||||
if self.mc['purge_action'] == 'delete':
|
||||
entry_result['changed'] = True
|
||||
self.r['diff']['before'][entry_name] = entry_cnf
|
||||
self.r['diff']['after'][entry_name] = None
|
||||
if not self.m.check_mode:
|
||||
entry.delete()
|
||||
|
||||
elif entry.b.is_enabled():
|
||||
entry_result['changed'] = True
|
||||
self.r['diff']['before'][entry_name] = {'enabled': True}
|
||||
self.r['diff']['after'][entry_name] = {'enabled': False}
|
||||
if not self.m.check_mode:
|
||||
entry.b.disable()
|
||||
|
||||
self._add_entry_result(entry, entry_result)
|
||||
|
||||
except ModuleSoftError:
|
||||
pass
|
||||
|
||||
def _in_entries_to_purge(self, entry_cnf: dict) -> bool:
|
||||
for purge_entry_cnf in self.p['multi_purge']:
|
||||
if self._entry_matches(entry_cnf, purge_entry_cnf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _in_entries_configured(self, entry_cnf: dict) -> bool:
|
||||
for configured_entry_cnf in self.p['multi']:
|
||||
if self._entry_matches(entry_cnf, configured_entry_cnf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _matches_purge_filter_partial(self, entry_cnf: dict) -> bool:
|
||||
matches = []
|
||||
for k, v in self.mc['purge_filter'].items():
|
||||
matches.append(str(entry_cnf[k]).find(str(v)) != -1)
|
||||
|
||||
return all(matches)
|
||||
|
||||
def _matches_purge_filter_full(self, entry_cnf: dict) -> bool:
|
||||
matches = []
|
||||
for k, v in self.mc['purge_filter'].items():
|
||||
matches.append(str(entry_cnf[k]) == str(v))
|
||||
|
||||
return all(matches)
|
||||
|
||||
def _matches_purge_filter(self, entry_cnf: dict) -> bool:
|
||||
# include in purge if matching & 'not inverted' - else exclude
|
||||
if self.mc['purge_filter_partial']:
|
||||
result = self._matches_purge_filter_partial(entry_cnf)
|
||||
|
||||
else:
|
||||
result = self._matches_purge_filter_full(entry_cnf)
|
||||
|
||||
if self.mc['purge_filter_invert']:
|
||||
return not result
|
||||
|
||||
return result
|
||||
|
||||
def _purge(self):
|
||||
if not self.mc['purge_all'] and not self._has_multi_purge_entries and not self._has_multi_purge_filters:
|
||||
self.m.fail_json("You need to either provide entries via 'multi_purge' or 'multi_control.purge_filter'!")
|
||||
|
||||
# checking all existing entries if they should be deleted
|
||||
for entry_cnf in self.cache['main']:
|
||||
if self.callbacks.purge_exclude(entry_cnf):
|
||||
continue
|
||||
|
||||
# user chose to purge_all, use a purge_filter or has provided multi_purge-entries that contain this entry
|
||||
purge_filter_all = self._has_multi_purge_filters and not self._has_multi_purge_entries
|
||||
if self.mc['purge_all'] or purge_filter_all or self._in_entries_to_purge(entry_cnf):
|
||||
# skip if purge_filters were provided, but do not match this entry
|
||||
if self._has_multi_purge_filters and not self._matches_purge_filter(entry_cnf):
|
||||
continue
|
||||
|
||||
if self.p['debug']:
|
||||
self.m.warn(f"Existing {self.k} '{self._entry_id(entry_cnf)}' will be {self.mc['purge_action']}d!")
|
||||
|
||||
self._purge_entry(entry_cnf)
|
||||
|
||||
def _purge_unconfigured(self):
|
||||
if not self.mc['purge_unconfigured']:
|
||||
return
|
||||
|
||||
# checking all pre-existing entries if they are contained in the user's config or should be deleted
|
||||
for entry_cnf in self._cache_original['main']:
|
||||
if self.callbacks.purge_exclude(entry_cnf) or self._in_entries_configured(entry_cnf):
|
||||
continue
|
||||
|
||||
if self.p['debug']:
|
||||
self.m.warn(f"Existing {self.k} '{self._entry_id(entry_cnf)}' will be {self.mc['purge_action']}d!")
|
||||
|
||||
self._purge_entry(entry_cnf)
|
||||
|
||||
# UTIL METHODS
|
||||
def _init_entry(self, entry_cnf: dict, entry_result: dict) -> BaseModule:
|
||||
entry_cnf['match_fields'] = self.match_fields
|
||||
o = self.o(
|
||||
module=self.m,
|
||||
result=entry_result,
|
||||
multi=entry_cnf,
|
||||
session=self.s,
|
||||
fail=dict(
|
||||
verify=self.mc['fail_verify'],
|
||||
process=self.mc['fail_process'],
|
||||
),
|
||||
)
|
||||
if self.cache_existing:
|
||||
self.callbacks.set_existing(o, cache=self.cache)
|
||||
|
||||
return o
|
||||
|
||||
def _entry_in_result(self, entry: (dict, BaseModule), before_after: str) -> bool:
|
||||
return self._entry_id(entry) in self.r['diff'][before_after]
|
||||
|
||||
def _add_entry_result(self, entry: BaseModule, entry_result: dict):
|
||||
if entry_result['changed']:
|
||||
self.r['changed'] = True
|
||||
|
||||
entry_result['diff'] = diff_remove_empty(entry_result['diff'], to_none=True)
|
||||
entry_name = self._entry_id(entry)
|
||||
|
||||
if 'before' in entry_result['diff']:
|
||||
self.r['diff']['before'][entry_name] = entry_result['diff']['before']
|
||||
|
||||
if 'after' in entry_result['diff']:
|
||||
self.r['diff']['after'][entry_name] = entry_result['diff']['after']
|
||||
|
||||
def _purge_unconfigured_clean_diff(self, entry: (dict, BaseModule)):
|
||||
# basically - nothing happened to this entry; so omit it from the diff
|
||||
entry_name = self._entry_id(entry)
|
||||
if entry_name in self.r['diff']['before'] and entry_name not in self.r['diff']['after']:
|
||||
self.r['diff']['before'].pop(entry_name)
|
||||
|
||||
def _entry_matches(self, e1: dict, e2: dict) -> bool:
|
||||
matches = []
|
||||
for field in self.match_fields:
|
||||
matches.append(
|
||||
field in e1 and
|
||||
field in e2 and
|
||||
str(e1[field]) == str(e2[field])
|
||||
)
|
||||
|
||||
res = all(matches)
|
||||
if not res and self.p['debug']:
|
||||
self.m.warn(f"Entries do not match: {e1} != {e2}")
|
||||
|
||||
return res
|
||||
|
||||
def _entry_id(self, entry: (dict, BaseModule)) -> str:
|
||||
entry_cnf = entry
|
||||
if isinstance(entry, BaseModule):
|
||||
entry_cnf = entry.p
|
||||
|
||||
if self.field_id in entry_cnf:
|
||||
return entry_cnf[self.field_id]
|
||||
|
||||
return 'NO-ID-FOUND'
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
from inspect import stack as inspect_stack
|
||||
from inspect import getfile as inspect_getfile
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.multi import \
|
||||
MultiModule, MultiModuleCallbacks
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.utils import profiler
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import diff_remove_empty
|
||||
|
||||
|
||||
def _single_module_process(instance: BaseModule):
|
||||
instance.check()
|
||||
instance.process()
|
||||
if 'reload' in instance.m.params and instance.r['changed'] and instance.m.params['reload']:
|
||||
instance.reload()
|
||||
|
||||
if hasattr(instance, 's'):
|
||||
instance.s.close()
|
||||
|
||||
instance.r['diff'] = diff_remove_empty(instance.r['diff'])
|
||||
|
||||
|
||||
def module_multi_wrapper(
|
||||
module: AnsibleModule, result: dict, obj: BaseModule, kind: str, entry_args: dict,
|
||||
callbacks: MultiModuleCallbacks = None,
|
||||
):
|
||||
m = MultiModule(
|
||||
module=module,
|
||||
result=result,
|
||||
kind=kind,
|
||||
obj=obj,
|
||||
entry_args=entry_args['multi']['options'],
|
||||
callbacks=callbacks,
|
||||
)
|
||||
if module.params['profiling'] or module.params['debug']:
|
||||
module_name = inspect_getfile(inspect_stack()[1][0]).rsplit('/', 1)[1].rsplit('.', 1)[0]
|
||||
return profiler(check=m.process, module_name=module_name, kwargs={})
|
||||
|
||||
# if the user wants to purge every entry - it makes no sense to hinder it for a single one
|
||||
if module.params['multi_control']['purge_all']:
|
||||
module.params['multi_control']['fail_process'] = False
|
||||
|
||||
return m.process()
|
||||
|
||||
|
||||
def module_wrapper(instance: BaseModule):
|
||||
if instance.m.params['profiling'] or instance.m.params['debug']:
|
||||
module_name = inspect_getfile(inspect_stack()[1][0]).rsplit('/', 1)[1].rsplit('.', 1)[0]
|
||||
return profiler(check=_single_module_process, module_name=module_name, kwargs={'instance': instance})
|
||||
|
||||
return _single_module_process(instance)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
STATE_MOD_ARG
|
||||
|
||||
|
||||
ALIAS_MOD_ARGS = dict(
|
||||
name=dict(type='str', required=True, aliases=['n']),
|
||||
description=dict(
|
||||
type='str', required=False, default='', aliases=['desc'],
|
||||
),
|
||||
content=dict(
|
||||
type='list', required=False, default=[], aliases=['c', 'cont'], elements='str',
|
||||
),
|
||||
type=dict(type='str', required=False, default='host', aliases=['t'], choices=[
|
||||
'host', 'network', 'port', 'url', 'urltable', 'geoip', 'networkgroup',
|
||||
'mac', 'dynipv6host', 'internal', 'external',
|
||||
]),
|
||||
updatefreq_days=dict(
|
||||
type='str', default='', required=False,
|
||||
description="Update frequency used by type 'urltable' in days - per example '0.5' for 12 hours"
|
||||
),
|
||||
interface=dict(
|
||||
type='str', default=None, aliases=['int', 'if'], required=False,
|
||||
description=' Select the interface for the V6 dynamic IP.',
|
||||
),
|
||||
path_expression=dict(
|
||||
type='str', default='', aliases=['pe', 'jq'], required=False,
|
||||
description='Simplified expression to select a field inside a container, a dot is used as field separator. '
|
||||
'Expressions using the jq language are also supported.',
|
||||
),
|
||||
**STATE_MOD_ARG,
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
STATE_MOD_ARG
|
||||
|
||||
|
||||
BIND_REC_MATCH_FIELDS = dict(
|
||||
match_fields=dict(
|
||||
type='list', elements='str',
|
||||
description='Fields that are used to match configured records with the running config - '
|
||||
"if any of those fields are changed, the module will think it's a new entry",
|
||||
choices=['name', 'domain', 'type', 'value'],
|
||||
default=['name', 'domain', 'type'], # required=False
|
||||
)
|
||||
)
|
||||
BIND_REC_MOD_ARGS = dict(
|
||||
domain=dict(type='str', required=True, aliases=['domain_name']),
|
||||
name=dict(type='str', required=True, aliases=['record']),
|
||||
type=dict(
|
||||
type='str', required=False, default='A',
|
||||
choices=[
|
||||
'A', 'AAAA', 'CAA', 'CNAME', 'DNSKEY', 'DS', 'MX', 'NS', 'PTR',
|
||||
'RRSIG', 'SRV', 'TLSA', 'TXT',
|
||||
]
|
||||
),
|
||||
value=dict(type='str', required=False),
|
||||
round_robin=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If multiple records with the same domain/name/type combination exist - '
|
||||
"the module will only execute 'state=absent' if set to 'false'. "
|
||||
"To create multiple ones set this to 'true'. "
|
||||
"Records will only be created, NOT UPDATED! (no matching is done)"
|
||||
),
|
||||
**BIND_REC_MATCH_FIELDS,
|
||||
**STATE_MOD_ARG,
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
IPSEC_AUTH_MOD_ARGS = dict(
|
||||
name=dict(
|
||||
type='str', required=True, aliases=['description', 'desc'],
|
||||
description='Unique name to identify the entry'
|
||||
),
|
||||
connection=dict(
|
||||
type='str', required=False, aliases=['tunnel', 'conn', 'tun'],
|
||||
description='Connection to use this local authentication with'
|
||||
),
|
||||
round=dict(
|
||||
type='int', required=False, default=0,
|
||||
description='Numeric identifier by which authentication rounds are sorted'
|
||||
),
|
||||
authentication=dict(
|
||||
type='str', required=False, aliases=['auth'], default='psk',
|
||||
choices=['psk', 'pubkey', 'eap-tls', 'eap-mschapv2', 'xauth-pam', 'eap-radius'],
|
||||
description='Authentication to perform for this round, when using Pre-Shared key make sure to define one '
|
||||
'under "VPN->IPsec->Pre-Shared Keys"',
|
||||
),
|
||||
id=dict(
|
||||
type='str', required=False, aliases=['ike_id'], default='',
|
||||
description='IKE identity to use for authentication round. When using certificate authentication. The IKE '
|
||||
'identity must be contained in the certificate, either as the subject DN or as a subjectAltName '
|
||||
'(the identity will default to the certificate’s subject DN if not specified). '
|
||||
'Refer to https://docs.strongswan.org/docs/5.9/config/identityParsing.html for details on '
|
||||
'how identities are parsed and may be configured'
|
||||
),
|
||||
eap_id=dict(
|
||||
type='str', required=False, default='',
|
||||
description='Client EAP-Identity to use in EAP-Identity exchange and the EAP method'
|
||||
),
|
||||
certificates=dict(
|
||||
type='list', elements='str', required=False, aliases=['certs'], default=[],
|
||||
description='List of certificate candidates to use for authentication'
|
||||
),
|
||||
public_keys=dict(
|
||||
type='list', elements='str', required=False, aliases=['pubkeys'], default=[],
|
||||
description='List of raw public key candidates to use for authentication'
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
PURGE_MOD_ARGS = dict(
|
||||
action=dict(
|
||||
type='str', required=False, default='delete', choices=['disable', 'delete'],
|
||||
description='What to do with the matched items'
|
||||
),
|
||||
filters=dict(
|
||||
type='dict', required=False, default={},
|
||||
description='Field-value pairs to filter on - per example: {param1: test} '
|
||||
"- to only purge items that have 'param1' set to 'test'"
|
||||
),
|
||||
filter_invert=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If true - it will purge all but the filtered ones'
|
||||
),
|
||||
filter_partial=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description="If true - the filter will also match if it is just a partial value-match"
|
||||
),
|
||||
force_all=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='If set to true and neither items, nor filters are provided - all items will be purged'
|
||||
),
|
||||
)
|
||||
|
||||
STATE_MOD_ARG_MULTI = dict(
|
||||
state=dict(type='str', required=False, choices=['present', 'absent']),
|
||||
enabled=dict(type='bool', required=False, default=None), # override only if set
|
||||
)
|
||||
|
||||
FAIL_MOD_ARG_MULTI = dict(
|
||||
fail_verification=dict(
|
||||
type='bool', required=False, default=False, aliases=['fail_verify'],
|
||||
description='Fail module if a single entry fails the verification.'
|
||||
),
|
||||
fail_processing=dict(
|
||||
type='bool', required=False, default=True, aliases=['fail_proc'],
|
||||
description='Fail module if a single entry fails to be processed.'
|
||||
),
|
||||
)
|
||||
|
||||
INFO_MOD_ARG = dict(
|
||||
output_info=dict(type='bool', required=False, default=False, aliases=['info']),
|
||||
)
|
||||
|
||||
RULE_MOD_ARG_KEY_FIELD = dict(
|
||||
key_field=dict(
|
||||
type='str', required=True, choices=['sequence', 'description', 'uuid'], aliases=['key'],
|
||||
description='What field is used as key of the provided dictionary'
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
OPN_MOD_ARGS = dict(
|
||||
firewall=dict(
|
||||
type='str', required=True,
|
||||
description="IP-Address or DNS hostname of the target firewall. "
|
||||
"Must be included as 'common name' or 'subject alternative name' in the firewalls web-certificate "
|
||||
"to use 'ssl_verify=true'"
|
||||
),
|
||||
api_port=dict(
|
||||
type='int', required=False, default=443,
|
||||
description='Port the target firewall uses for its web-interface'
|
||||
),
|
||||
api_key=dict(
|
||||
type='str', required=False, no_log=True,
|
||||
description="API key used to authenticate, alternative to 'api_credential_file'"
|
||||
),
|
||||
api_secret=dict(
|
||||
type='str', required=False, no_log=True,
|
||||
description="API secret used to authenticate, alternative to 'api_credential_file'. "
|
||||
"Is set as 'no_log' parameter"
|
||||
),
|
||||
api_credential_file=dict(
|
||||
type='path', required=False,
|
||||
description="Path to the api-credential file as downloaded through the web-interface. "
|
||||
"Alternative to 'api_key' and 'api_secret'"
|
||||
),
|
||||
ssl_verify=dict(
|
||||
type='bool', required=False, default=True,
|
||||
description='If the certificate of the target firewall should be validated. RECOMMENDED FOR PRODUCTION USAGE!'
|
||||
),
|
||||
ssl_ca_file=dict(
|
||||
type='path', required=False,
|
||||
description='If you use an internal certificate-authority to create the certificate of the target firewall, '
|
||||
'provide the path to its public key for validation'
|
||||
),
|
||||
debug=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description="Used to en-/disable the debug mode. All API requests and responses will be shown "
|
||||
"as Ansible warnings at runtime. Will be hidden if the tasks 'no_log' parameter is set to 'true'"
|
||||
),
|
||||
profiling=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description="Used to en-/disable the profiling mode. "
|
||||
"Time consumption of the module will be logged to '/tmp/oxlorg.opnsense'"
|
||||
),
|
||||
api_timeout=dict(
|
||||
type='float', required=False, aliases=['timeout'],
|
||||
description='Manually override the modules default API-request timeout'
|
||||
),
|
||||
api_retries=dict(
|
||||
type='int', required=False, default=0, aliases=['connect_retries'],
|
||||
description='Number of retries on API requests, in case there is an error when establishing the connection. '
|
||||
'This does not handle errors returned by the OPNsense system'
|
||||
),
|
||||
)
|
||||
|
||||
BUILTIN_ALIASES = [
|
||||
'bogons', 'bogonsv6', 'sshlockout', 'virusprot', '__wazuh_agent_drop',
|
||||
]
|
||||
BUILTIN_INTERFACE_ALIASES_REG = '^__.*?_network$' # auto-added interface aliases
|
||||
|
||||
STATE_ONLY_MOD_ARG = dict(
|
||||
state=dict(type='str', required=False, choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
EN_ONLY_MOD_ARG = dict(
|
||||
enabled=dict(type='bool', required=False, default=True),
|
||||
)
|
||||
|
||||
STATE_MOD_ARG = dict(
|
||||
**STATE_ONLY_MOD_ARG,
|
||||
**EN_ONLY_MOD_ARG,
|
||||
)
|
||||
|
||||
RELOAD_MOD_ARG = dict(
|
||||
reload=dict(
|
||||
type='bool', required=False, default=True, aliases=['apply'],
|
||||
description='If the running config should be reloaded/applied on change - '
|
||||
'will take some time'
|
||||
)
|
||||
)
|
||||
|
||||
RELOAD_MOD_ARG_DEF_FALSE = dict(
|
||||
reload=dict(
|
||||
type='bool', required=False, default=False, aliases=['apply'],
|
||||
description='If the running config should be reloaded on change - '
|
||||
'will take some time'
|
||||
)
|
||||
)
|
||||
|
||||
DEBUG_CONFIG = dict(
|
||||
path_log='/tmp/oxlorg.opnsense',
|
||||
log_api_calls='api_calls.log',
|
||||
)
|
||||
|
||||
CONNECTION_TEST_TIMEOUT = 1.5
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
OPENVPN_INSTANCE_MOD_ARGS = dict(
|
||||
name=dict(
|
||||
type='str', required=True, aliases=['desc', 'description'],
|
||||
description='The name used to match this config to existing entries'
|
||||
),
|
||||
protocol=dict(
|
||||
type='str', required=False, default='udp', aliases=['proto'],
|
||||
choices=['udp', 'udp4', 'udp6', 'tcp', 'tcp4', 'tcp6'],
|
||||
description='Use protocol for communicating with remote host.'
|
||||
),
|
||||
address=dict(
|
||||
type='str', required=False, default='', aliases=['bind_address', 'ip', 'bind'],
|
||||
description='Optional IP address for bind.'
|
||||
'If specified, OpenVPN will bind to this address only.'
|
||||
'If unspecified, OpenVPN will bind to all interfaces.'
|
||||
),
|
||||
mode=dict(
|
||||
type='str', required=False, default='tun', aliases=['type'], choices=['tun', 'tap'],
|
||||
description='Choose the type of tunnel, OSI Layer 3 [tun] is the most common option '
|
||||
'to route IPv4 or IPv6 traffic, [tap] offers Ethernet 802.3 (OSI Layer 2) connectivity '
|
||||
'between hosts and is usually combined with a bridge.'
|
||||
),
|
||||
log_level=dict(
|
||||
type='int', required=False, default=3, aliases=['verbosity', 'verb'],
|
||||
choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
description='Output verbosity level. 0 = no output, 1-4 = normal, 5 = log packets, 6-11 debug'
|
||||
),
|
||||
keepalive_interval=dict(
|
||||
type='str', required=False, default='', aliases=['kai'],
|
||||
description='Ping interval in seconds. 0 to disable keep alive'
|
||||
),
|
||||
keepalive_timeout=dict(
|
||||
type='str', required=False, default='', aliases=['kat'],
|
||||
description='Causes OpenVPN to restart after n seconds pass without reception of a '
|
||||
'ping or other packet from remote.'
|
||||
),
|
||||
# trust
|
||||
certificate=dict(
|
||||
type='str', required=False, aliases=['cert'],
|
||||
description='Certificate to use for this service.'
|
||||
),
|
||||
verify_remote_certificate=dict(
|
||||
type='bool', required=False, default=False,
|
||||
description='Require that the peer certificate was signed with an explicit "key usage" and '
|
||||
'"extended key usage" based on RFC 3280 rules.'
|
||||
),
|
||||
ca=dict(
|
||||
type='str', required=False, default='', aliases=['certificate_authority', 'authority'],
|
||||
description='Select a certificate authority when it differs from the attached certificate.'
|
||||
),
|
||||
key=dict(
|
||||
type='str', required=False, default='', aliases=['tls_key', 'tls_static_key'],
|
||||
description='Add an additional layer of HMAC authentication on top of the TLS control channel to '
|
||||
'mitigate DoS attacks and attacks on the TLS stack. The prefixed mode determines if '
|
||||
'this measurement is only used for authentication (--tls-auth) or includes encryption '
|
||||
'(--tls-crypt).'
|
||||
),
|
||||
authentication=dict(
|
||||
type='str', required=False, default='', aliases=['auth', 'auth_algo'],
|
||||
choices=[
|
||||
'', 'BLAKE2b512', 'BLAKE2s256', 'whirlpool', 'none',
|
||||
'MD4', 'MD5', 'MD5-SHA1', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA3-224', 'SHA3-256',
|
||||
'SHA3-384', 'SHA3-512', 'SHA384', 'SHA512', 'SHA512-224', 'SHA512-256', 'SHAKE128', 'SHAKE256',
|
||||
],
|
||||
description='Authenticate data channel packets and (if enabled) tls-auth control channel packets with '
|
||||
'HMAC using message digest algorithm alg.'
|
||||
),
|
||||
# auth
|
||||
renegotiate_time=dict(
|
||||
type='str', required=False, default='', aliases=['reneg_time', 'reneg'],
|
||||
description='Renegotiate data channel key after n seconds (default=3600). When using a one time '
|
||||
'password, be advised that your connection will automatically drop because your '
|
||||
'password is not valid anymore. Set to 0 to disable, remember to change your '
|
||||
'client as well.'
|
||||
),
|
||||
# routing
|
||||
network_local=dict(
|
||||
type='list', elements='str', required=False, default=[], aliases=['net_local', 'push_route'],
|
||||
description='These are the networks accessible on this host, these are pushed via route{-ipv6} '
|
||||
'clauses in OpenVPN to the client.'
|
||||
),
|
||||
network_remote=dict(
|
||||
type='list', elements='str', required=False, default=[], aliases=['net_remote', 'route'],
|
||||
description='Remote networks for the server, add route to routing table after connection is established'
|
||||
),
|
||||
# misc
|
||||
options=dict(
|
||||
type='list', elements='str', required=False, default=[], aliases=['opts'],
|
||||
description='Various less frequently used yes/no options which can be set for this instance.',
|
||||
choices=[
|
||||
'client-to-client', 'duplicate-cn', 'passtos', 'persist-remote-ip', 'route-nopull', 'route-noexec',
|
||||
'remote-random', 'float',
|
||||
],
|
||||
),
|
||||
mtu=dict(
|
||||
type='str', required=False, default='', aliases=['tun_mtu'],
|
||||
description='Take the TUN device MTU to be tun-mtu and derive the link MTU from it.'
|
||||
),
|
||||
fragment_size=dict(
|
||||
type='str', required=False, default='', aliases=['frag_size'],
|
||||
description='Enable internal datagram fragmentation so that no UDP datagrams are sent which are larger '
|
||||
'than the specified byte size.'
|
||||
),
|
||||
mss_fix=dict(
|
||||
type='bool', required=False, default=False, aliases=['mss'],
|
||||
description='Announce to TCP sessions running over the tunnel that they should limit their send packet '
|
||||
'sizes such that after OpenVPN has encapsulated them, the resulting UDP packet size that '
|
||||
'OpenVPN sends to its peer will not exceed the recommended size.'
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import STATE_MOD_ARG
|
||||
|
||||
RULE_DEFAULTS = {
|
||||
'sequence': 1,
|
||||
'action': 'pass',
|
||||
'quick': True,
|
||||
'interface': ['lan'],
|
||||
'interface_invert': False,
|
||||
'direction': 'in',
|
||||
'ip_protocol': 'inet',
|
||||
'protocol': 'any',
|
||||
'source_invert': False,
|
||||
'source_net': 'any',
|
||||
'source_port': '',
|
||||
'destination_invert': False,
|
||||
'destination_net': 'any',
|
||||
'destination_port': '',
|
||||
'gateway': '',
|
||||
'replyto': '',
|
||||
'disable_replyto': False,
|
||||
'log': True,
|
||||
'allow_opts': False,
|
||||
'state_type': 'keep',
|
||||
'state_policy': '',
|
||||
'state_timeout': None,
|
||||
'max_states': None,
|
||||
'max_src_nodes': None,
|
||||
'max_src_states': None,
|
||||
'max_src_conn': None,
|
||||
'max_src_conn_rate': None,
|
||||
'max_src_conn_rates': None,
|
||||
'overload': None,
|
||||
'adaptive_start': None,
|
||||
'adaptive_end': None,
|
||||
'prio': '',
|
||||
'set_prio': '',
|
||||
'set_prio_low': '',
|
||||
'tag': '',
|
||||
'tagged': '',
|
||||
'tcp_flags': [],
|
||||
'tcp_flags_clear': [],
|
||||
'schedule': '',
|
||||
'tos': '',
|
||||
'state': 'present',
|
||||
'enabled': True,
|
||||
'description': '',
|
||||
'debug': False,
|
||||
'icmp_type': [],
|
||||
}
|
||||
|
||||
RULE_MOD_ARG_ALIASES = {
|
||||
'sequence': ['seq'],
|
||||
'action': ['a'],
|
||||
'quick': ['q'],
|
||||
'interface': ['int', 'i'],
|
||||
'interface_invert': ['int_inv', 'ii', 'int_not'],
|
||||
'direction': ['dir'],
|
||||
'ip_protocol': ['ip', 'ip_proto'],
|
||||
'protocol': ['proto', 'p'],
|
||||
'source_invert': ['src_inv', 'si', 'src_not'],
|
||||
'source_net': ['source', 'src', 's'],
|
||||
'source_port': ['src_port', 'sp'],
|
||||
'destination_invert': ['dest_inv', 'di', 'dest_not'],
|
||||
'destination_net': ['destination', 'dest', 'd'],
|
||||
'destination_port': ['dest_port', 'dp'],
|
||||
'gateway': ['gw', 'g'],
|
||||
'replyto': ['rt'],
|
||||
'log': ['l'],
|
||||
'allow_opts': ['opts'],
|
||||
'overload': ['ol'],
|
||||
'schedule': ['sched'],
|
||||
'description': ['name', 'desc'],
|
||||
'state': ['st'],
|
||||
'enabled': ['en'],
|
||||
'icmp_type': ['icmp_types'],
|
||||
}
|
||||
|
||||
RULE_MATCH_FIELDS_ARG = dict(
|
||||
match_fields=dict(
|
||||
type='list', required=True, elements='str',
|
||||
description='Fields that are used to match configured rules with the running config - '
|
||||
"if any of those fields are changed, the module will think it's a new rule",
|
||||
choices=[
|
||||
'sequence', 'action', 'interface', 'direction', 'ip_protocol', 'protocol',
|
||||
'source_invert', 'source_net', 'source_port', 'destination_invert', 'destination_net',
|
||||
'destination_port', 'gateway', 'description', 'uuid',
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
RULE_MOD_ARGS = dict(
|
||||
sequence=dict(
|
||||
type='int', required=False, default=RULE_DEFAULTS['sequence'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['sequence']
|
||||
),
|
||||
action=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['action'], choices=['pass', 'block', 'reject'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['action']
|
||||
),
|
||||
quick=dict(type='bool', required=False, default=RULE_DEFAULTS['quick'], aliases=RULE_MOD_ARG_ALIASES['quick']),
|
||||
interface=dict(
|
||||
type='list', required=False, default=RULE_DEFAULTS['interface'], aliases=RULE_MOD_ARG_ALIASES['interface'],
|
||||
description='One or multiple interfaces use this rule on', elements='str',
|
||||
),
|
||||
interface_invert=dict(
|
||||
type='bool', required=False, default=RULE_DEFAULTS['interface_invert'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['interface_invert'],
|
||||
),
|
||||
direction=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['direction'], aliases=RULE_MOD_ARG_ALIASES['direction'],
|
||||
choices=['in', 'out']
|
||||
),
|
||||
ip_protocol=dict(
|
||||
type='str', required=False, choices=['inet', 'inet6', 'inet46'],
|
||||
default=RULE_DEFAULTS['ip_protocol'], description="IPv4 = 'inet', IPv6 = 'inet6', 'IPv4+6 = 'inet46'",
|
||||
aliases=RULE_MOD_ARG_ALIASES['ip_protocol'],
|
||||
),
|
||||
protocol=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['protocol'], aliases=RULE_MOD_ARG_ALIASES['protocol'],
|
||||
description="Protocol like 'TCP', 'UDP', 'ICMP', 'TCP/UDP' and so on."
|
||||
),
|
||||
source_invert=dict(
|
||||
type='bool', required=False, default=RULE_DEFAULTS['source_invert'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['source_invert'],
|
||||
),
|
||||
source_net=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['source_net'], aliases=RULE_MOD_ARG_ALIASES['source_net'],
|
||||
description="Host, network, alias or 'any'",
|
||||
),
|
||||
source_port=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['source_port'], aliases=RULE_MOD_ARG_ALIASES['source_port'],
|
||||
description='Leave empty to allow all, valid port-number, name, alias or range'
|
||||
),
|
||||
destination_invert=dict(
|
||||
type='bool', required=False, default=RULE_DEFAULTS['destination_invert'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['destination_invert'],
|
||||
),
|
||||
destination_net=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['destination_net'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['destination_net'], description="Host, network, alias or 'any'"
|
||||
),
|
||||
destination_port=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['destination_port'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['destination_port'],
|
||||
description='Leave empty to allow all, valid port-number, name, alias or range'
|
||||
),
|
||||
gateway=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['gateway'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['gateway'], description='Existing gateway to use'
|
||||
),
|
||||
replyto=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['replyto'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['replyto'],
|
||||
description='Determines how packets route back in the opposite direction'
|
||||
),
|
||||
disable_replyto=dict(
|
||||
type='bool', required=False, default=RULE_DEFAULTS['disable_replyto'],
|
||||
description='Explicit disable reply-to for this rule'
|
||||
),
|
||||
log=dict(type='bool', required=False, default=RULE_DEFAULTS['log'], aliases=RULE_MOD_ARG_ALIASES['log'],),
|
||||
allow_opts=dict(
|
||||
type='bool', required=False, default=RULE_DEFAULTS['allow_opts'], aliases=RULE_MOD_ARG_ALIASES['allow_opts'],
|
||||
description='Allows packets with IP options to pass'
|
||||
),
|
||||
state_type=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['state_type'],
|
||||
choices=['keep', 'sloppy', 'modulate', 'synproxy', 'none'],
|
||||
description='State tracking mechanism to use'
|
||||
),
|
||||
state_policy=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['state_policy'],
|
||||
choices=['', 'if-bound', 'floating'], description='State tracking mechanism to use'
|
||||
),
|
||||
state_timeout=dict(
|
||||
type='int', required=False,
|
||||
description='State Timeout in seconds (TCP only)'
|
||||
),
|
||||
max_states=dict(type='int', required=False, description='Limits the number of concurrent states',),
|
||||
max_src_nodes=dict(
|
||||
type='int', required=False,
|
||||
description='Limits the number of source addresses which can simultaneously have state table entries'
|
||||
),
|
||||
max_src_states=dict(
|
||||
type='int', required=False,
|
||||
description='Limits the number of simultaneous state entries that a single source address can create'
|
||||
),
|
||||
max_src_conn=dict(
|
||||
type='int', required=False,
|
||||
description='Limit the number of simultaneous TCP connections a single host can make'
|
||||
),
|
||||
max_src_conn_rate=dict(
|
||||
type='int', required=False,
|
||||
description='Maximum new connections per host, measured over time'
|
||||
),
|
||||
max_src_conn_rates=dict(
|
||||
type='int', required=False,
|
||||
description='Time interval (seconds) to measure the number of connections'
|
||||
),
|
||||
overload=dict(
|
||||
type='str', required=False,
|
||||
aliases=RULE_MOD_ARG_ALIASES['overload'],
|
||||
description='Overload table used when max new connections per time interval has been reached'
|
||||
),
|
||||
adaptive_start=dict(type='int', required=False,),
|
||||
adaptive_end=dict(type='int', required=False,),
|
||||
prio=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['prio'],
|
||||
choices=['', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
description='Match packets which have the given queueing priority assigned'
|
||||
),
|
||||
set_prio=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['set_prio'],
|
||||
choices=['', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
description='Assigne a specific queueing priority'
|
||||
),
|
||||
set_prio_low=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['set_prio_low'],
|
||||
choices=['', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
description='Assigne a specific queueing priority to packets which have a TOS of lowdelay '
|
||||
'and TCP ACKs with no data payload'
|
||||
),
|
||||
tag=dict(type='str', required=False, default=RULE_DEFAULTS['tag'],),
|
||||
tagged=dict(type='str', required=False, default=RULE_DEFAULTS['tagged'],),
|
||||
tcp_flags=dict(
|
||||
type='list', elements='str', required=False, default=RULE_DEFAULTS['tcp_flags'],
|
||||
choices=['syn', 'ack', 'fin', 'rst', 'psh', 'urg', 'ece', 'cwr'],
|
||||
description='TCP flags that must be set for this rule to match'
|
||||
),
|
||||
tcp_flags_clear=dict(
|
||||
type='list', elements='str', required=False, default=RULE_DEFAULTS['tcp_flags_clear'],
|
||||
choices=['syn', 'ack', 'fin', 'rst', 'psh', 'urg', 'ece', 'cwr'],
|
||||
description='TCP flags that must be cleared for this rule to match'
|
||||
),
|
||||
schedule=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['schedule'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['schedule'],
|
||||
),
|
||||
tos=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['tos'],
|
||||
description='Match packets which have the given TOS/DCSP assigned'
|
||||
),
|
||||
description=dict(
|
||||
type='str', required=False, default=RULE_DEFAULTS['description'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['description']
|
||||
),
|
||||
uuid=dict(type='str', required=False, description='Optionally you can supply the uuid of an existing rule'),
|
||||
icmp_type=dict(
|
||||
type='list', elements='str', required=False, default=RULE_DEFAULTS['icmp_type'],
|
||||
aliases=RULE_MOD_ARG_ALIASES['icmp_type'], choices=[
|
||||
'echoreq', 'echorep', 'unreach', 'squench', 'redir', 'althost', 'routeradv', 'routersol', 'timex',
|
||||
'paramprob', 'timereq', 'timerep', 'inforeq', 'inforep', 'maskreq', 'maskrep',
|
||||
],
|
||||
description='If protocol is ICMP/IPV6-ICMP you can specify the types'
|
||||
),
|
||||
**STATE_MOD_ARG,
|
||||
**RULE_MATCH_FIELDS_ARG,
|
||||
)
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
from re import match as regex_match
|
||||
from typing import Callable
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
BUILTIN_ALIASES, BUILTIN_INTERFACE_ALIASES_REG
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_valid_partial_mac_address, is_valid_url, validate_port_or_range, is_valid_network, is_valid_host, is_ip
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
|
||||
|
||||
# This should be kept aligned with getValidators from the AliasContentField
|
||||
# https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
|
||||
def validate_values(cnf: dict, error_func: Callable, existing_entries: dict) -> None:
|
||||
v_type = cnf['type']
|
||||
|
||||
if isinstance(existing_entries, dict):
|
||||
existing_entries = [
|
||||
a['name']
|
||||
for a in existing_entries.values()
|
||||
]
|
||||
|
||||
else:
|
||||
existing_entries = [
|
||||
a['name']
|
||||
for a in existing_entries
|
||||
]
|
||||
|
||||
for value in cnf['content']:
|
||||
if value in existing_entries:
|
||||
continue
|
||||
|
||||
error = f"Value '{value}' is invalid for type '{v_type}'!"
|
||||
|
||||
if v_type == 'port':
|
||||
validate_port_or_range(module=None, port=value, error_func=error_func, range_sep=':')
|
||||
|
||||
elif v_type == 'host' and not is_valid_host(value):
|
||||
error_func(error)
|
||||
|
||||
#elif v_type == 'geoip':
|
||||
# pass
|
||||
|
||||
elif v_type == 'network' and not is_valid_network(value):
|
||||
error_func(error)
|
||||
|
||||
elif v_type == 'networkgroup':
|
||||
error_func(f"Value '{value}' is not a valid alias!")
|
||||
|
||||
elif v_type == 'mac' and not is_valid_partial_mac_address(value):
|
||||
error_func(error)
|
||||
|
||||
elif v_type == 'dynipv6host' and not is_ip(f"0000{value}"):
|
||||
error_func(error)
|
||||
|
||||
elif v_type == 'asn':
|
||||
try:
|
||||
if int(value) < 1 or int(value) > 4294967296:
|
||||
error_func(error)
|
||||
|
||||
except ValueError:
|
||||
error_func(error)
|
||||
|
||||
#elif v_type == 'authgroup':
|
||||
# pass
|
||||
|
||||
# OPNsense has no validation for urls in the API
|
||||
elif v_type in ['url', 'urltable', 'urljson'] and not is_valid_url(value):
|
||||
error_func(error)
|
||||
|
||||
|
||||
def compare_aliases(existing: dict, configured: dict) -> tuple:
|
||||
before = list(map(str, existing['content']))
|
||||
after = list(map(str, configured['content']))
|
||||
before.sort()
|
||||
after.sort()
|
||||
return before != after, before, after
|
||||
|
||||
|
||||
def builtin_alias(name: str) -> bool:
|
||||
# ignore built-in aliases
|
||||
return name in BUILTIN_ALIASES or \
|
||||
regex_match(BUILTIN_INTERFACE_ALIASES_REG, name) is not None
|
||||
|
||||
|
||||
def filter_builtin_alias(aliases: list) -> list:
|
||||
filtered = []
|
||||
|
||||
for alias in aliases:
|
||||
# ignore built-in aliases
|
||||
if not builtin_alias(alias['name']):
|
||||
filtered.append(alias)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
DEFAULT_UPDATEFREQ_DAYS_URLTABLE = 7 # see: https://github.com/O-X-L/ansible-opnsense/pull/270
|
||||
|
||||
|
||||
def build_updatefreq(updatefreq: (int, float, str), default: bool = False) -> (int, float):
|
||||
if is_unset(updatefreq):
|
||||
if default:
|
||||
return DEFAULT_UPDATEFREQ_DAYS_URLTABLE
|
||||
|
||||
return updatefreq
|
||||
|
||||
updatefreq = float(updatefreq)
|
||||
dec = 1
|
||||
if str(updatefreq).endswith('.0'):
|
||||
dec = None
|
||||
|
||||
return round(updatefreq, dec)
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import ssl
|
||||
from pathlib import Path
|
||||
from json import JSONDecodeError
|
||||
from json import dumps as json_dumps
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
DEBUG_CONFIG
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
ensure_list
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_valid_domain, is_ip
|
||||
|
||||
|
||||
def _load_credential_file(module: AnsibleModule) -> tuple[(str, None), (str, None)]:
|
||||
cred_file_info = Path(module.params['api_credential_file'])
|
||||
|
||||
if cred_file_info.is_file():
|
||||
cred_file_mode = oct(cred_file_info.stat().st_mode)[-3:]
|
||||
|
||||
if int(cred_file_mode[2]) != 0:
|
||||
module.warn(
|
||||
f"Provided 'api_credential_file' at path "
|
||||
f"'{module.params['api_credential_file']}' is world-readable "
|
||||
f"(mode {cred_file_mode})!"
|
||||
)
|
||||
|
||||
with open(module.params['api_credential_file'], 'r', encoding='utf-8') as file:
|
||||
config = {}
|
||||
vaulted = False
|
||||
|
||||
for line in file.readlines():
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
config[key] = value.strip()
|
||||
|
||||
except ValueError:
|
||||
if line.startswith('$ANSIBLE_VAULT'):
|
||||
vaulted = True
|
||||
break
|
||||
|
||||
if vaulted:
|
||||
module.fail_json(
|
||||
f"Credential file '{module.params['api_credential_file']}' "
|
||||
'is ansible-vault encrypted! This is not yet supported.'
|
||||
)
|
||||
|
||||
if 'key' not in config or 'secret' not in config:
|
||||
module.fail_json(
|
||||
f"Credential file '{module.params['api_credential_file']}' "
|
||||
'could not be parsed!'
|
||||
)
|
||||
|
||||
return config['key'], config['secret']
|
||||
|
||||
else:
|
||||
module.fail_json(
|
||||
f"Provided 'api_credential_file' at path "
|
||||
f"'{module.params['api_credential_file']}' does not exist!"
|
||||
)
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def check_or_load_credentials(module: AnsibleModule) -> tuple[(str, None), (str, None)]:
|
||||
if module.params['api_credential_file'] is not None:
|
||||
return _load_credential_file(module)
|
||||
|
||||
if module.params['api_key'] is None and module.params['api_secret'] is None:
|
||||
module.fail_json("Neither 'api_key' & 'api_secret' nor 'api_credential_file' were provided!")
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def check_host(module: AnsibleModule) -> None:
|
||||
if not is_ip(module.params['firewall']):
|
||||
fw_dns = module.params['firewall']
|
||||
|
||||
if fw_dns.find('.') == -1:
|
||||
# TLD-only will fail the domain validation
|
||||
fw_dns = f'dummy.{fw_dns}'
|
||||
|
||||
if not is_valid_domain(fw_dns):
|
||||
module.fail_json(f"Host '{module.params['firewall']}' is neither a valid IP nor Domain-Name!")
|
||||
|
||||
|
||||
def ssl_verification(module: AnsibleModule) -> (ssl.SSLContext, bool):
|
||||
context = ssl.create_default_context()
|
||||
|
||||
if not module.params['ssl_verify']:
|
||||
context = False
|
||||
|
||||
elif module.params['ssl_ca_file'] is not None:
|
||||
if Path(module.params['ssl_ca_file']).is_file():
|
||||
context.load_verify_locations(cafile=module.params['ssl_ca_file'])
|
||||
|
||||
else:
|
||||
module.fail_json(f"Provided 'ssl_ca_file' at path '{module.params['ssl_ca_file']}' does not exist!")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_params_path(cnf: dict) -> str:
|
||||
params_path = ''
|
||||
|
||||
if 'params' in cnf and cnf['params'] is not None:
|
||||
for param in ensure_list(cnf['params']):
|
||||
params_path += f"/{param}"
|
||||
|
||||
return params_path
|
||||
|
||||
|
||||
def _clean_response(response: dict) -> dict:
|
||||
response_data = response.copy()
|
||||
for field in [
|
||||
'headers', 'next_request', '_decoder', 'stream', 'extensions', 'history', 'is_closed',
|
||||
'is_stream_consumed', 'default_encoding',
|
||||
]:
|
||||
if field in response_data:
|
||||
response_data.pop(field)
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
def debug_api(
|
||||
module: AnsibleModule, method: str = None, url: str = None,
|
||||
data: dict = None, headers: dict = None, response: dict = None,
|
||||
) -> None:
|
||||
if 'debug' not in module.params or not module.params['debug']:
|
||||
return
|
||||
|
||||
if response is not None:
|
||||
msg = f"RESPONSE: '{_clean_response(response.__dict__)}'"
|
||||
|
||||
else:
|
||||
msg = f'REQUEST: {method} | URL: {url}'
|
||||
|
||||
if headers is not None:
|
||||
msg += f" | HEADERS: '{headers}'"
|
||||
|
||||
if data is not None:
|
||||
msg += f" | DATA: '{json_dumps(data)}'"
|
||||
|
||||
log_path = Path(DEBUG_CONFIG['path_log'])
|
||||
if not log_path.exists():
|
||||
log_path.mkdir()
|
||||
|
||||
with open(
|
||||
f"{log_path}/{DEBUG_CONFIG['log_api_calls']}",
|
||||
'a+', encoding='utf-8'
|
||||
) as log:
|
||||
log.write(f"\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} | {method} => {url}\n")
|
||||
|
||||
module.warn(msg)
|
||||
|
||||
|
||||
def check_response(module: AnsibleModule, cnf: dict, response) -> dict:
|
||||
debug_api(module=module, response=response)
|
||||
|
||||
if 'allowed_http_stati' not in cnf:
|
||||
cnf['allowed_http_stati'] = [200]
|
||||
|
||||
try:
|
||||
json = response.json()
|
||||
|
||||
except JSONDecodeError:
|
||||
json = {}
|
||||
|
||||
if response.status_code not in cnf['allowed_http_stati'] or \
|
||||
('result' in json and json['result'] == 'failed'):
|
||||
# sometimes an error 'hides' behind a 200-code
|
||||
if f"{response.__dict__}".find('Controller not found') != -1:
|
||||
module.fail_json(
|
||||
f"API call failed | Needed plugin not installed! | "
|
||||
f"Response: {response.__dict__}"
|
||||
)
|
||||
|
||||
elif f"{response.__dict__}".find(' in use') != -1:
|
||||
json['in_use'] = True
|
||||
|
||||
else:
|
||||
if 'validations' in json:
|
||||
module.fail_json(
|
||||
f"API call failed | Error: {json['validations']} | "
|
||||
f"Response: {response.__dict__}"
|
||||
)
|
||||
|
||||
else:
|
||||
module.fail_json(f"API call failed | Response: {response.__dict__}")
|
||||
|
||||
return json
|
||||
|
||||
|
||||
def api_pretty_exception(m: AnsibleModule, method: str, url: str, error):
|
||||
call = f'{method} => {url}'
|
||||
msg = f"Unable to connect '{call}'"
|
||||
|
||||
if str(error).find('timed out') != -1:
|
||||
msg = f"Got timeout calling '{call}'"
|
||||
|
||||
if str(error).find('CERTIFICATE_VERIFY_FAILED') != -1 or str(error).find('certificate verify failed') != -1:
|
||||
msg = f"SSL verification failed '{url}'! Make sure to follow the the documentation: "\
|
||||
"https://ansible-opnsense.oxl.app/usage/2_basic.html#ssl-certificate"
|
||||
|
||||
m.fail_json(f"{msg} ({error})")
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
from typing import Callable
|
||||
from functools import reduce
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import \
|
||||
exit_bug, exit_cnf
|
||||
|
||||
|
||||
def diff_remove_empty(diff: dict, to_none: bool = False) -> dict:
|
||||
d = diff.copy()
|
||||
for k in diff:
|
||||
if len(diff[k]) == 0:
|
||||
if to_none:
|
||||
d[k] = None
|
||||
|
||||
else:
|
||||
d.pop(k)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def ensure_list(data: (int, str, list, None)) -> list:
|
||||
# if user supplied a string instead of a list => convert it to match our expectations
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
|
||||
if data is None:
|
||||
return []
|
||||
|
||||
return [data]
|
||||
|
||||
|
||||
def get_matching(
|
||||
module: AnsibleModule, existing_items: (dict, list), compare_item: dict,
|
||||
match_fields: list, simplify_func: Callable = None,
|
||||
) -> (dict, None):
|
||||
matching = None
|
||||
|
||||
if len(existing_items) > 0:
|
||||
if isinstance(existing_items, dict):
|
||||
_existing_items_list = []
|
||||
for uuid, existing in existing_items.items():
|
||||
existing['uuid'] = uuid
|
||||
_existing_items_list.append(existing)
|
||||
|
||||
existing_items = _existing_items_list
|
||||
|
||||
for existing in existing_items:
|
||||
_matching = []
|
||||
|
||||
if simplify_func is not None:
|
||||
existing = simplify_func(existing)
|
||||
|
||||
try:
|
||||
if not isinstance(match_fields, list):
|
||||
exit_bug(f"Failed because 'match_fields' are not a list: {type(match_fields)} '{match_fields}'")
|
||||
|
||||
for field in match_fields:
|
||||
_matching.append(str(existing[field]) == str(compare_item[field]))
|
||||
|
||||
if module.params['debug']:
|
||||
if existing[field] != compare_item[field]:
|
||||
module.warn(
|
||||
f"NOT MATCHING: "
|
||||
f"'{existing[field]}' != '{compare_item[field]}'"
|
||||
)
|
||||
|
||||
except KeyError as error:
|
||||
exit_bug(
|
||||
"Failed to match existing entry with provided one: "
|
||||
f"{existing} <=> {sanitize_module_args(compare_item)}; "
|
||||
f"Error while comparing: {error}"
|
||||
)
|
||||
|
||||
if all(_matching):
|
||||
matching = existing
|
||||
break
|
||||
|
||||
return matching
|
||||
|
||||
|
||||
def get_multiple_matching(
|
||||
module: AnsibleModule, existing_items: (dict, list), compare_item: dict,
|
||||
match_fields: list, simplify_func: Callable = None,
|
||||
) -> list:
|
||||
matching = []
|
||||
|
||||
if len(existing_items) > 0:
|
||||
if isinstance(existing_items, dict):
|
||||
_existing_items_list = []
|
||||
for uuid, existing in existing_items.items():
|
||||
existing['uuid'] = uuid
|
||||
_existing_items_list.append(existing)
|
||||
|
||||
existing_items = _existing_items_list
|
||||
|
||||
for existing in existing_items:
|
||||
_simple = get_matching(
|
||||
module=module,
|
||||
existing_items=[existing],
|
||||
compare_item=compare_item,
|
||||
match_fields=match_fields,
|
||||
simplify_func=simplify_func,
|
||||
)
|
||||
if _simple is not None:
|
||||
matching.append(_simple)
|
||||
|
||||
return matching
|
||||
|
||||
|
||||
def is_true(data: (str, int, bool)) -> bool:
|
||||
return data in [1, '1', True]
|
||||
|
||||
|
||||
def get_selected(data: dict) -> (str, None):
|
||||
if isinstance(data, dict):
|
||||
for key, values in data.items():
|
||||
if is_true(values['selected']):
|
||||
return key
|
||||
|
||||
return '' # none selected
|
||||
|
||||
# if function is re-applied
|
||||
return data
|
||||
|
||||
|
||||
def get_selected_value(data: (dict, list)) -> (str, None):
|
||||
if isinstance(data, dict):
|
||||
for values in data.values():
|
||||
if is_true(values['selected']) and 'value' in values:
|
||||
return values['value']
|
||||
|
||||
return '' # none selected
|
||||
|
||||
if isinstance(data, list):
|
||||
for values in data:
|
||||
if is_true(values['selected']) and 'value' in values:
|
||||
return values['value']
|
||||
|
||||
return '' # none selected
|
||||
|
||||
# if function is re-applied
|
||||
return data
|
||||
|
||||
|
||||
def get_selected_opt_list(data: (dict, list)) -> (str, None):
|
||||
if isinstance(data, dict):
|
||||
return get_selected(data)
|
||||
|
||||
return get_selected_value(data)
|
||||
|
||||
|
||||
def get_selected_opt_list_idx(data: list) -> int:
|
||||
idx = 0
|
||||
for values in data:
|
||||
if is_true(values['selected']):
|
||||
return idx
|
||||
|
||||
idx += 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def get_selected_multi(data: (dict, list), get_value: bool = False) -> list:
|
||||
if isinstance(data, list) and len(data) > 0 and not isinstance(data[0], dict):
|
||||
# if function is re-applied
|
||||
return data
|
||||
|
||||
selected_values = []
|
||||
if isinstance(data, dict):
|
||||
for key, values in data.items():
|
||||
if is_true(values['selected']):
|
||||
if not get_value:
|
||||
selected_values.append(key)
|
||||
|
||||
if get_value and 'value' in values:
|
||||
selected_values.append(values['value'])
|
||||
|
||||
if isinstance(data, list):
|
||||
for values in data:
|
||||
if is_true(values['selected']) and 'value' in values:
|
||||
selected_values.append(values['value'])
|
||||
|
||||
return selected_values
|
||||
|
||||
|
||||
def get_selected_list(data: dict, remove_empty: bool = False, get_value: bool = False) -> list:
|
||||
if isinstance(data, list):
|
||||
if len(data) == 0:
|
||||
return []
|
||||
|
||||
if not isinstance(data[0], dict):
|
||||
# if function is re-applied
|
||||
return data
|
||||
|
||||
if isinstance(data, str):
|
||||
if data.strip() == '':
|
||||
return []
|
||||
|
||||
return data.split(',')
|
||||
|
||||
selected = get_selected_multi(data=data, get_value=get_value)
|
||||
if remove_empty:
|
||||
for key in [None, '', ' ']:
|
||||
if key in selected:
|
||||
selected.remove(key)
|
||||
|
||||
if 'System: Deny config write' in selected:
|
||||
raise exit_bug(f"TEST: {data}")
|
||||
|
||||
selected.sort()
|
||||
return selected
|
||||
|
||||
|
||||
def get_key_by_value_from_selection(selection: dict, value: str) -> (str, None):
|
||||
if isinstance(selection, dict):
|
||||
for key, values in selection.items():
|
||||
if 'value' in values and values['value'] == value:
|
||||
return key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_key_by_value_end_from_selection(selection: dict, value: str) -> (str, None):
|
||||
if isinstance(selection, dict):
|
||||
for key, values in selection.items():
|
||||
if 'value' in values and values['value'].endswith(value):
|
||||
return key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_key_by_value_beg_from_selection(selection: dict, value: str) -> (str, None):
|
||||
if isinstance(selection, dict):
|
||||
for key, values in selection.items():
|
||||
if 'value' in values and values['value'].startswith(value):
|
||||
return key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def to_digit(data: bool) -> int:
|
||||
return 1 if data else 0
|
||||
|
||||
|
||||
def get_simple_existing(
|
||||
entries: (dict, list), add_filter: Callable = None,
|
||||
simplify_func: Callable = None
|
||||
) -> list:
|
||||
simple_entries = []
|
||||
|
||||
if isinstance(entries, dict):
|
||||
_entries = []
|
||||
for uuid, entry in entries.items():
|
||||
if not isinstance(entry, dict):
|
||||
exit_bug(f"The provided entry is not a dictionary => '{entry}'")
|
||||
|
||||
entry['uuid'] = uuid
|
||||
_entries.append(entry)
|
||||
|
||||
entries = _entries
|
||||
|
||||
for entry in entries:
|
||||
if simplify_func is not None and add_filter is not None:
|
||||
simple_entries.append(add_filter(simplify_func(entry)))
|
||||
|
||||
elif simplify_func is not None:
|
||||
simple_entries.append(simplify_func(entry))
|
||||
|
||||
else:
|
||||
simple_entries.append(entries)
|
||||
|
||||
return simple_entries
|
||||
|
||||
|
||||
def format_int(data: (int, str)) -> (int, str, None):
|
||||
if isinstance(data, int):
|
||||
return data
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if data.isnumeric():
|
||||
return int(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sort_param_lists(params: dict) -> None:
|
||||
for k in params:
|
||||
if isinstance(params[k], list):
|
||||
try:
|
||||
params[k].sort()
|
||||
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# pylint: disable=R0914,R0915
|
||||
def simplify_translate(
|
||||
existing: dict, translate: dict = None, typing: dict = None,
|
||||
bool_invert: list = None, ignore: list = None, value_map: dict = None,
|
||||
) -> dict:
|
||||
# pylint: disable=R0912
|
||||
simple = {}
|
||||
if translate is None:
|
||||
translate = {}
|
||||
|
||||
if typing is None:
|
||||
typing = {}
|
||||
|
||||
if bool_invert is None:
|
||||
bool_invert = []
|
||||
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
|
||||
if value_map is None:
|
||||
value_map = {}
|
||||
|
||||
try:
|
||||
# translate api-fields to ansible-fields
|
||||
for k, v in translate.items():
|
||||
if v in existing:
|
||||
simple[k] = existing[v]
|
||||
elif isinstance(v, tuple):
|
||||
simple[k] = reduce(lambda e, i: e[i], v, existing)
|
||||
|
||||
translate_fields = translate.values()
|
||||
for k in existing:
|
||||
if k not in translate_fields and k not in ignore:
|
||||
simple[k] = existing[k]
|
||||
|
||||
# correct value types to match (for diff-checks)
|
||||
for t, fields in typing.items():
|
||||
for f in fields:
|
||||
if f in ignore:
|
||||
continue
|
||||
|
||||
if t == 'bool':
|
||||
simple[f] = is_true(simple[f])
|
||||
|
||||
elif t == 'int':
|
||||
simple[f] = format_int(simple[f])
|
||||
|
||||
elif t == 'list':
|
||||
simple[f] = get_selected_list(data=simple[f], remove_empty=True, get_value=False)
|
||||
|
||||
elif t == 'list_value':
|
||||
simple[f] = get_selected_list(data=simple[f], remove_empty=True, get_value=True)
|
||||
|
||||
elif t == 'select':
|
||||
simple[f] = get_selected(simple[f])
|
||||
|
||||
elif t == 'select_opt_list':
|
||||
simple[f] = get_selected_opt_list(simple[f])
|
||||
|
||||
elif t == 'select_opt_list_idx':
|
||||
simple[f] = get_selected_opt_list_idx(simple[f])
|
||||
|
||||
for f, vmap in value_map.items():
|
||||
try:
|
||||
for pretty_value, opn_value in vmap.items():
|
||||
if simple[f] == opn_value:
|
||||
simple[f] = pretty_value
|
||||
break
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for k, v in simple.items():
|
||||
if isinstance(v, str) and v.isnumeric():
|
||||
simple[k] = int(simple[k])
|
||||
|
||||
elif isinstance(v, bool) and k in bool_invert:
|
||||
simple[k] = not simple[k]
|
||||
|
||||
except KeyError as err:
|
||||
exit_bug(
|
||||
f"Failed to translate API entry to Ansible entry! Maybe the API changed lately? "
|
||||
f"Failed field: {err} | "
|
||||
f"API entry: '{existing}' '{simple}'"
|
||||
)
|
||||
|
||||
return simple
|
||||
|
||||
|
||||
def is_unset(value: (str, None, list, dict)) -> bool:
|
||||
if isinstance(value, (list, dict)):
|
||||
return len(value) == 0
|
||||
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
|
||||
return value in ['', None]
|
||||
|
||||
|
||||
def unset_check_error(params: dict, field: str, fail: bool) -> bool:
|
||||
if is_unset(params[field]):
|
||||
if fail:
|
||||
exit_cnf(f"Field '{field}' must be set!")
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def sanitize_module_args(args: dict) -> dict:
|
||||
args.pop('api_key', None)
|
||||
args.pop('api_secret', None)
|
||||
return args
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def validate_values(error_func, module: AnsibleModule, cnf: dict, kind: str = 'filter') -> None:
|
||||
# error = "Value '%s' is invalid for the field '%s'!"
|
||||
|
||||
# can't validate as aliases are supported
|
||||
# for field in ['source_net', 'destination_net']:
|
||||
# if cnf[field] not in [None, '', 'any']:
|
||||
# try:
|
||||
# ip_network(cnf[field])
|
||||
#
|
||||
# except ValueError:
|
||||
# try:
|
||||
# ip_address(cnf[field])
|
||||
#
|
||||
# except ValueError:
|
||||
# error_func(error % (cnf[field], field))
|
||||
|
||||
if kind == 'filter':
|
||||
required_together=[
|
||||
('max_src_conn_rate', 'max_src_conn_rates', 'overload'),
|
||||
('adaptive_start', 'adaptive_end'),
|
||||
('tcp_flags', 'tcp_flags_clear'),
|
||||
]
|
||||
for opts in required_together:
|
||||
if any((cnf[opts[0]] is None) != (cnf[o] is None) for o in opts[1:]):
|
||||
error_func(f"parameters are required together: {', '.join(opts)}")
|
||||
if any((cnf[opts[0]] == []) != (cnf[o] == []) for o in opts[1:]):
|
||||
error_func(f"parameters are required together: {', '.join(opts)}")
|
||||
|
||||
# some recommendations - maybe the user overlooked something
|
||||
if 'action' in cnf and cnf['action'] == 'pass' and cnf['protocol'] in ['TCP', 'UDP', 'TCP/UDP']:
|
||||
if cnf['source_net'] == 'any' and cnf['destination_net'] == 'any':
|
||||
module.warn(
|
||||
"Configuring allow-rules with 'any' source and "
|
||||
"'any' destination is bad practice!"
|
||||
)
|
||||
|
||||
elif cnf['destination_net'] == 'any' and cnf['destination_port'] == 'any':
|
||||
module.warn(
|
||||
"Configuring allow-rules to 'any' destination "
|
||||
"using 'all' ports is bad practice!"
|
||||
)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
from socket import socket, AF_INET, AF_INET6, SOCK_STREAM, gaierror
|
||||
from time import time, sleep
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session, HTTPX_EXCEPTIONS
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import CONNECTION_TEST_TIMEOUT
|
||||
|
||||
|
||||
def _opn_reachable_ipv(module: AnsibleModule, address_family: int) -> bool:
|
||||
with socket(address_family, SOCK_STREAM) as s:
|
||||
s.settimeout(CONNECTION_TEST_TIMEOUT)
|
||||
return s.connect_ex((
|
||||
module.params['firewall'],
|
||||
module.params['api_port']
|
||||
)) == 0
|
||||
|
||||
|
||||
def _opn_reachable(module: AnsibleModule) -> bool:
|
||||
try:
|
||||
return _opn_reachable_ipv(module, AF_INET)
|
||||
|
||||
except gaierror:
|
||||
return _opn_reachable_ipv(module, AF_INET6)
|
||||
|
||||
|
||||
def _wait_msg(module: AnsibleModule, msg: str):
|
||||
module.warn(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | {msg}")
|
||||
|
||||
|
||||
def wait_for_response(module: AnsibleModule) -> bool:
|
||||
timeout = time() + module.params['wait_timeout']
|
||||
|
||||
_wait_msg(module, 'Waiting for services to stop..')
|
||||
sleep(10)
|
||||
|
||||
while time() < timeout:
|
||||
poll_interval_start = time()
|
||||
|
||||
if _opn_reachable(module=module):
|
||||
_wait_msg(module, 'Got response!')
|
||||
return True
|
||||
|
||||
_wait_msg(module, 'Waiting for response..')
|
||||
poll_interval_elapsed = time() - poll_interval_start
|
||||
if poll_interval_elapsed < module.params['poll_interval']:
|
||||
sleep(module.params['poll_interval'] - poll_interval_elapsed)
|
||||
|
||||
raise TimeoutError
|
||||
|
||||
|
||||
def get_upgrade_status(s: Session) -> dict:
|
||||
return s.get({
|
||||
'command': 'upgradestatus',
|
||||
'module': 'core',
|
||||
'controller': 'firmware',
|
||||
})
|
||||
|
||||
|
||||
def wait_for_update(module: AnsibleModule, s: Session) -> bool:
|
||||
timeout = time() + module.params['wait_timeout']
|
||||
|
||||
if module.params['action'] == 'upgrade':
|
||||
_wait_msg(module, 'Waiting for download & upgrade to finish..')
|
||||
|
||||
else:
|
||||
_wait_msg(module, 'Waiting for update to finish..')
|
||||
|
||||
sleep(2)
|
||||
|
||||
while time() < timeout:
|
||||
poll_interval_start = time()
|
||||
|
||||
try:
|
||||
result = get_upgrade_status(s)
|
||||
status = result['status']
|
||||
|
||||
_wait_msg(module, f"Got response: {status}")
|
||||
|
||||
if status == 'error' and 'log' in result:
|
||||
_wait_msg(module, f"Got error: {result['log']}")
|
||||
return False
|
||||
|
||||
if status == 'done':
|
||||
_wait_msg(module, f"Got result: {result['log']}")
|
||||
return True
|
||||
|
||||
except (HTTPX_EXCEPTIONS, ConnectionError, TimeoutError):
|
||||
# not reachable while rebooting
|
||||
_wait_msg(module, 'Waiting for response..')
|
||||
|
||||
poll_interval_elapsed = time() - poll_interval_start
|
||||
if poll_interval_elapsed < module.params['poll_interval']:
|
||||
sleep(module.params['poll_interval'] - poll_interval_elapsed)
|
||||
|
||||
raise TimeoutError
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_valid_domain
|
||||
|
||||
|
||||
def validate_domain(module: AnsibleModule, domain: str) -> None:
|
||||
test_domain = domain
|
||||
|
||||
if domain.find('.') == -1:
|
||||
# TLD-only will fail the domain validation
|
||||
test_domain = f'dummy.{domain}'
|
||||
|
||||
if not is_valid_domain(test_domain):
|
||||
module.fail_json(f"Value '{domain}' is an invalid domain!")
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from cProfile import Profile
|
||||
from pstats import Stats
|
||||
from io import StringIO
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from inspect import stack as inspect_stack
|
||||
from inspect import getfile as inspect_getfile
|
||||
|
||||
from httpx import ConnectError, ConnectTimeout
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.defaults.main import \
|
||||
DEBUG_CONFIG
|
||||
|
||||
|
||||
def profiler(
|
||||
check: Callable, kwargs: dict, module_name: str = None,
|
||||
sort: str = 'tottime', show_top_n: int = 20
|
||||
) -> (list, dict, bool, None):
|
||||
# note: https://stackoverflow.com/questions/10326936/sort-cprofile-output-by-percall-when-profiling-a-python-script
|
||||
# sort options: ncalls, tottime, cumtime
|
||||
_ = Profile()
|
||||
_.enable()
|
||||
|
||||
if module_name is None:
|
||||
module_name = inspect_getfile(inspect_stack()[1][0]).rsplit('/', 1)[1].rsplit('.', 1)[0]
|
||||
|
||||
httpx_error = None
|
||||
check_response = None
|
||||
|
||||
try:
|
||||
check_response = check(**kwargs)
|
||||
|
||||
except (ConnectError, ConnectTimeout, ConnectionError) as error:
|
||||
httpx_error = str(error)
|
||||
|
||||
_.disable()
|
||||
result = StringIO()
|
||||
Stats(_, stream=result).sort_stats(sort).print_stats(show_top_n)
|
||||
cleaned_result = result.getvalue().splitlines()[:-1]
|
||||
del cleaned_result[1:5]
|
||||
cleaned_result = '\n'.join(cleaned_result)
|
||||
|
||||
if module_name is not None:
|
||||
log_path = Path(DEBUG_CONFIG['path_log'])
|
||||
if not log_path.exists():
|
||||
log_path.mkdir()
|
||||
|
||||
with open(f'{log_path}/{module_name}.log', 'a+', encoding='utf-8') as log:
|
||||
log.write(f"\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} | {cleaned_result}\n")
|
||||
|
||||
else:
|
||||
print(cleaned_result)
|
||||
|
||||
if httpx_error is not None:
|
||||
print(f"HTTP ERROR: {httpx_error}")
|
||||
|
||||
return check_response
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
from typing import Callable
|
||||
from re import match as regex_match
|
||||
from re import compile as regex_compile
|
||||
from re import IGNORECASE as REGEX_IGNORECASE
|
||||
from re import UNICODE as REGEX_UNICODE
|
||||
from socket import getservbyname
|
||||
from ipaddress import ip_address, ip_network, IPv4Address, IPv6Address, IPv6Network, AddressValueError, \
|
||||
NetmaskValueError
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
# pylint: disable=W0611
|
||||
# (proxied imports)
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset, ensure_list, is_true, unset_check_error
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import \
|
||||
exit_bug
|
||||
|
||||
MATCH_DOMAIN = regex_compile(
|
||||
r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|'
|
||||
r'([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|'
|
||||
r'([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.'
|
||||
r'([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$'
|
||||
)
|
||||
MATCH_EMAIL_USER = regex_compile(
|
||||
# dot-atom
|
||||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+"
|
||||
r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
|
||||
# quoted-string
|
||||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|'
|
||||
r"""\\[\001-\011\013\014\016-\177])*"$)""",
|
||||
REGEX_IGNORECASE
|
||||
)
|
||||
MATCH_EMAIL_DOMAIN = regex_compile(
|
||||
# domain
|
||||
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
|
||||
r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)'
|
||||
# literal form, ipv4 address (SMTP 4.1.3)
|
||||
r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
|
||||
r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$',
|
||||
REGEX_IGNORECASE
|
||||
)
|
||||
IP_MIDDLE_OCTET = r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))"
|
||||
IP_LAST_OCTET = r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))"
|
||||
MATCH_URL_RAW = regex_compile(
|
||||
r"^"
|
||||
# protocol identifier
|
||||
r"(?:(?:https?|ftp)://)"
|
||||
# user:pass authentication
|
||||
r"(?:\S+(?::\S*)?@)?"
|
||||
r"(?:"
|
||||
r"(?P<private_ip>"
|
||||
# IP address exclusion
|
||||
# private & local networks
|
||||
r"(?:(?:10|127)" + IP_MIDDLE_OCTET + r"{2}" + IP_LAST_OCTET + r")|"
|
||||
r"(?:(?:169\.254|192\.168)" + IP_MIDDLE_OCTET + IP_LAST_OCTET + r")|"
|
||||
r"(?:172\.(?:1[6-9]|2\d|3[0-1])" + IP_MIDDLE_OCTET + IP_LAST_OCTET + r"))"
|
||||
r"|"
|
||||
# IP address dotted notation octets
|
||||
# excludes loopback network 0.0.0.0
|
||||
# excludes reserved space >= 224.0.0.0
|
||||
# excludes network & broadcast addresses
|
||||
# (first & last IP address of each class)
|
||||
r"(?P<public_ip>"
|
||||
r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])"
|
||||
r"" + IP_MIDDLE_OCTET + r"{2}"
|
||||
r"" + IP_LAST_OCTET + r")"
|
||||
r"|"
|
||||
# host name
|
||||
r"(?:(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)"
|
||||
# domain name
|
||||
r"(?:\.(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)*"
|
||||
# TLD identifier
|
||||
r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))"
|
||||
r")"
|
||||
# port number
|
||||
r"(?::\d{2,5})?"
|
||||
# resource path
|
||||
r"(?:/\S*)?"
|
||||
# query string
|
||||
r"(?:\?\S*)?"
|
||||
r"$",
|
||||
REGEX_UNICODE | REGEX_IGNORECASE
|
||||
)
|
||||
MATCH_URL = regex_compile(MATCH_URL_RAW)
|
||||
MATCH_MAC_ADDRESS = regex_compile(r'^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$')
|
||||
MATCH_PARTIAL_MAC_ADDRESS = regex_compile(r'^(?:[0-9a-fA-F]{2}:){1,5}[0-9a-fA-F]{2}$')
|
||||
# see: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
MATCH_HOSTNAME = regex_compile(r'^[a-zA-Z0-9-\.]{1,253}$')
|
||||
MATCH_UUID = regex_compile(r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$')
|
||||
|
||||
|
||||
def _is_matching(compiled_regex, value: (str, None)) -> bool:
|
||||
if value is None:
|
||||
value = ''
|
||||
|
||||
return compiled_regex.match(value) is not None
|
||||
|
||||
|
||||
def is_valid_domain(value: str) -> bool:
|
||||
# see: https://validators.readthedocs.io/en/latest/_modules/validators/domain.html#domain
|
||||
return _is_matching(compiled_regex=MATCH_DOMAIN, value=value)
|
||||
|
||||
|
||||
def is_valid_email(value) -> bool:
|
||||
# see: https://validators.readthedocs.io/en/latest/_modules/validators/email.html
|
||||
if not value or '@' not in value:
|
||||
return False
|
||||
|
||||
email_user, email_domain = value.rsplit('@', 1)
|
||||
|
||||
if not _is_matching(compiled_regex=MATCH_EMAIL_USER, value=email_user):
|
||||
return False
|
||||
|
||||
if not _is_matching(compiled_regex=MATCH_EMAIL_DOMAIN, value=email_domain):
|
||||
# Try for possible IDN domain-part
|
||||
try:
|
||||
domain_part = email_domain.encode('idna').decode('ascii')
|
||||
return _is_matching(compiled_regex=MATCH_EMAIL_DOMAIN, value=domain_part)
|
||||
|
||||
except UnicodeError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_valid_url(value: str) -> bool:
|
||||
# see: https://validators.readthedocs.io/en/latest/_modules/validators/url.html
|
||||
return _is_matching(compiled_regex=MATCH_URL, value=value)
|
||||
|
||||
|
||||
def is_valid_mac_address(value: str) -> bool:
|
||||
# see: https://validators.readthedocs.io/en/latest/_modules/validators/mac_address.html
|
||||
return _is_matching(compiled_regex=MATCH_MAC_ADDRESS, value=value)
|
||||
|
||||
|
||||
def is_valid_partial_mac_address(value: str) -> bool:
|
||||
# see: https://validators.readthedocs.io/en/latest/_modules/validators/mac_address.html
|
||||
return _is_matching(compiled_regex=MATCH_PARTIAL_MAC_ADDRESS, value=value)
|
||||
|
||||
|
||||
def is_valid_network(value: str) -> bool:
|
||||
if '-' in value:
|
||||
for _value in value.split('-', 1):
|
||||
if not is_ip(_value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
value = value.lstrip('!')
|
||||
return is_ip_or_network(value)
|
||||
|
||||
|
||||
def is_valid_host(value: str) -> bool:
|
||||
if is_valid_domain(value):
|
||||
return True
|
||||
|
||||
for _value in value.split('-', 1):
|
||||
_value = _value.strip('!')
|
||||
if not is_ip(_value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_port(module: AnsibleModule, port: int, error_func: Callable = None) -> bool:
|
||||
if error_func is None:
|
||||
error_func = module.fail_json
|
||||
|
||||
if is_unset(port):
|
||||
return True
|
||||
|
||||
if 1 <= int(port) <= 65535:
|
||||
return True
|
||||
|
||||
error_func(f"Value '{port}' is an invalid port!")
|
||||
return False
|
||||
|
||||
|
||||
def validate_port_or_range(module: AnsibleModule, port: str, error_func: Callable = None, range_sep: str = '-') -> bool:
|
||||
if error_func is None:
|
||||
error_func = module.fail_json
|
||||
|
||||
if port == 'any' or is_unset(port):
|
||||
return True
|
||||
|
||||
for _value in port.split(range_sep, 1):
|
||||
if _value.isdecimal() and (1 <= int(_value) <= 65535):
|
||||
continue
|
||||
|
||||
try:
|
||||
getservbyname(_value)
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
error_func(f"Value '{port}' is an invalid port or range!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_int_fields(
|
||||
module: AnsibleModule, data: dict, field_minmax: dict,
|
||||
error_func: Callable = None
|
||||
):
|
||||
if error_func is None:
|
||||
error_func = module.fail_json
|
||||
|
||||
for field, valid in field_minmax.items():
|
||||
try:
|
||||
if ('min' in valid and int(data[field]) < valid['min']) or \
|
||||
('max' in valid and int(data[field]) > valid['max']):
|
||||
error_func(
|
||||
f"Value of field '{field}' is not valid - "
|
||||
f"Must be between {valid['min']} and {valid['max']}!"
|
||||
)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_str_fields(
|
||||
module: AnsibleModule, data: dict, field_regex: dict = None,
|
||||
field_minmax_length: dict = None, allow_empty: bool = False,
|
||||
) -> None:
|
||||
if field_minmax_length is not None:
|
||||
for field, min_max_length in field_minmax_length.items():
|
||||
if not allow_empty and 'min' in min_max_length and min_max_length['min'] == 0:
|
||||
allow_empty = True
|
||||
|
||||
if not unset_check_error(params=data, field=field, fail=not allow_empty):
|
||||
continue
|
||||
|
||||
if 'min' not in min_max_length or 'max' not in min_max_length:
|
||||
exit_bug("Values of 'STR_LEN_VALIDATIONS' must have a 'min' and 'max' attribute!")
|
||||
|
||||
if min_max_length['min'] < len(str(data[field])) > min_max_length['max']:
|
||||
module.fail_json(
|
||||
f"Value of field '{field}' is not valid - "
|
||||
f"Invalid length must be between {min_max_length['min']} and {min_max_length['max']}!"
|
||||
)
|
||||
|
||||
if field_regex is not None:
|
||||
for field, regex in field_regex.items():
|
||||
if not unset_check_error(params=data, field=field, fail=not allow_empty):
|
||||
continue
|
||||
|
||||
if regex_match(regex, data[field]) is None:
|
||||
module.fail_json(
|
||||
f"Value of field '{field}' is not valid - "
|
||||
f"Must match regex '{regex}'!"
|
||||
)
|
||||
|
||||
|
||||
def is_ip(host: str, ignore_empty: bool = False, strip_enclosure: bool = True) -> bool:
|
||||
if ignore_empty and is_unset(host):
|
||||
return True
|
||||
|
||||
if strip_enclosure and host.startswith('['):
|
||||
host = host[1:-1]
|
||||
|
||||
try:
|
||||
ip_address(host)
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_ip4(host: str, ignore_empty: bool = False) -> bool:
|
||||
if ignore_empty and is_unset(host):
|
||||
return True
|
||||
|
||||
try:
|
||||
IPv4Address(host)
|
||||
return True
|
||||
|
||||
except (AddressValueError, NetmaskValueError):
|
||||
return False
|
||||
|
||||
|
||||
def is_ip6(host: str, ignore_empty: bool = False, strip_enclosure: bool = True) -> bool:
|
||||
if ignore_empty and is_unset(host):
|
||||
return True
|
||||
|
||||
if strip_enclosure and host.startswith('['):
|
||||
host = host[1:-1]
|
||||
|
||||
try:
|
||||
IPv6Address(host)
|
||||
return True
|
||||
|
||||
except (AddressValueError, NetmaskValueError):
|
||||
return False
|
||||
|
||||
|
||||
def is_network(entry: str, strict: bool = False) -> bool:
|
||||
try:
|
||||
ip_network(entry, strict=strict)
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_ip_or_network(entry: str, strict: bool = False) -> bool:
|
||||
valid = is_ip(entry)
|
||||
|
||||
if valid:
|
||||
return valid
|
||||
|
||||
return is_network(entry=entry, strict=strict)
|
||||
|
||||
|
||||
def is_ip6_network(entry: str, strict: bool = False) -> bool:
|
||||
try:
|
||||
return isinstance(ip_network(entry, strict=strict), IPv6Network)
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def valid_hostname(name: str) -> bool:
|
||||
_valid_domain = is_valid_domain(name)
|
||||
_valid_hostname = _is_matching(compiled_regex=MATCH_HOSTNAME, value=name)
|
||||
return all([_valid_domain, _valid_hostname])
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Account(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add',
|
||||
'del': 'del',
|
||||
'set': 'update',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle',
|
||||
}
|
||||
API_KEY_PATH = 'acmeclient.accounts.account'
|
||||
API_MOD = 'acmeclient'
|
||||
API_CONT = 'accounts'
|
||||
API_CONT_GET = 'settings'
|
||||
FIELDS_CHANGE = ['description', 'custom_ca', 'eab_kid', 'eab_hmac']
|
||||
FIELDS_ALL = [
|
||||
'enabled', 'name', 'email', 'ca',
|
||||
]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['ca'],
|
||||
}
|
||||
EXIST_ATTR = 'account'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.account = {}
|
||||
|
||||
def process(self) -> None:
|
||||
self.b.process()
|
||||
|
||||
if self.p['state'] == 'present' and self.p['register']:
|
||||
self.register()
|
||||
|
||||
def register(self) -> None:
|
||||
if self.account.get('statusCode', 100) == 200:
|
||||
return
|
||||
|
||||
self.r['changed'] = True
|
||||
if not self.m.check_mode:
|
||||
cont_get, mod_get = self.API_CONT, self.API_MOD
|
||||
self.call_cnf['controller'] = cont_get
|
||||
self.call_cnf['module'] = mod_get
|
||||
self.s.post(cnf={
|
||||
**self.call_cnf,
|
||||
'command': 'register',
|
||||
})
|
||||
|
||||
def reload(self):
|
||||
# no reload required
|
||||
pass
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Action(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add',
|
||||
'del': 'del',
|
||||
'set': 'update',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle',
|
||||
}
|
||||
API_KEY_PATH = 'acmeclient.actions.action'
|
||||
API_MOD = 'acmeclient'
|
||||
API_CONT = 'actions'
|
||||
API_CONT_GET = 'settings'
|
||||
FIELDS_CHANGE = ['type']
|
||||
FIELDS_ALL = [
|
||||
'enabled', 'name', 'description',
|
||||
# SFTP
|
||||
'sftp_host', 'sftp_host_key', 'sftp_port', 'sftp_user', 'sftp_identity_type',
|
||||
'sftp_remote_path', 'sftp_chgrp', 'sftp_chmod', 'sftp_chmod_key',
|
||||
'sftp_filename_cert', 'sftp_filename_key', 'sftp_filename_ca',
|
||||
'sftp_filename_fullchain',
|
||||
# Remote SSH
|
||||
'remote_ssh_host', 'remote_ssh_host_key', 'remote_ssh_port', 'remote_ssh_user',
|
||||
'remote_ssh_identity_type', 'remote_ssh_command',
|
||||
# ACME FRITZ!Box
|
||||
'acme_fritzbox_url', 'acme_fritzbox_username', 'acme_fritzbox_password',
|
||||
# ACME PANOS
|
||||
'acme_panos_username', 'acme_panos_password', 'acme_panos_host',
|
||||
# ACME promox VE
|
||||
'acme_proxmoxve_user', 'acme_proxmoxve_server', 'acme_proxmoxve_port',
|
||||
'acme_proxmoxve_nodename', 'acme_proxmoxve_realm', 'acme_proxmoxve_tokenid',
|
||||
'acme_proxmoxve_tokenkey',
|
||||
# ACME Vault
|
||||
'acme_vault_url', 'acme_vault_prefix', 'acme_vault_token', 'acme_vault_kvv2',
|
||||
# ACME Synology DSM
|
||||
'acme_synology_dsm_hostname', 'acme_synology_dsm_port', 'acme_synology_dsm_scheme',
|
||||
'acme_synology_dsm_username', 'acme_synology_dsm_password', 'acme_synology_dsm_create',
|
||||
'acme_synology_dsm_deviceid', 'acme_synology_dsm_devicename',
|
||||
# ACME TrueNAS
|
||||
'acme_truenas_apikey', 'acme_truenas_hostname', 'acme_truenas_scheme',
|
||||
# ACME unifi
|
||||
'acme_unifi_keystore',
|
||||
]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'acme_vault_kvv2', 'acme_synology_dsm_create'],
|
||||
'select': [
|
||||
'type', 'remote_ssh_identity_type', 'acme_synology_dsm_scheme', 'acme_truenas_scheme',
|
||||
'sftp_identity_type',
|
||||
],
|
||||
'int': ['sftp_port', 'remote_ssh_port', 'acme_proxmoxve_port', 'acme_synology_dsm_port'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'sftp_port': {'min': 1, 'max': 65535},
|
||||
}
|
||||
EXIST_ATTR = 'action'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.action = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['type']):
|
||||
self.m.fail_json('You need to provide type to create/update actions!')
|
||||
|
||||
if self.p['type'].startswith('acme_'):
|
||||
for field in self.FIELDS_ALL:
|
||||
if field.startswith(self.p['type']) and is_unset(self.p[field]):
|
||||
self.m.fail_json(f"You need to provide {field} to create/update {self.p['type']} actions!")
|
||||
|
||||
self._base_check()
|
||||
|
||||
def reload(self):
|
||||
# no reload required
|
||||
pass
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Certificate(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add',
|
||||
'del': 'del',
|
||||
'set': 'update',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle',
|
||||
}
|
||||
API_KEY_PATH = 'acmeclient.certificates.certificate'
|
||||
API_MOD = 'acmeclient'
|
||||
API_CONT = 'certificates'
|
||||
API_CONT_GET = 'settings'
|
||||
FIELDS_CHANGE = [
|
||||
'name', 'alt_names', 'account', 'validation', 'restart_actions', 'auto_renewal', 'renew_interval', 'aliasmode',
|
||||
'key_length', 'ocsp'
|
||||
]
|
||||
FIELDS_ALL = [
|
||||
'enabled', 'description', 'domainalias', 'challengealias'
|
||||
]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'alt_names': 'altNames',
|
||||
'validation': 'validationMethod',
|
||||
'key_length': 'keyLength',
|
||||
'restart_actions': 'restartActions',
|
||||
'auto_renewal': 'autoRenewal',
|
||||
'renew_interval': 'renewInterval',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'auto_renewal', 'ocsp'],
|
||||
'list': ['alt_names', 'restart_actions'],
|
||||
'select': ['account', 'validation', 'restart_actions', 'aliasmode', 'key_length'],
|
||||
'int': ['renew_interval'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'renew_interval': {'min': 1, 'max': 5000},
|
||||
}
|
||||
EXIST_ATTR = 'certificate'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_accounts': 'acmeclient.accounts.account',
|
||||
'existing_validations': 'acmeclient.validations.validation',
|
||||
'existing_actions': 'acmeclient.actions.action',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.certificate = {}
|
||||
self.existing_accounts = {}
|
||||
self.existing_validations = {}
|
||||
self.existing_actions = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['name']):
|
||||
self.m.fail_json('You need to provide a name to create/update certificates!')
|
||||
|
||||
if self.p['aliasmode'] == 'domain':
|
||||
self.FIELDS_CHANGE.append('domainalias')
|
||||
|
||||
elif self.p['aliasmode'] == 'challenge':
|
||||
self.FIELDS_CHANGE.append('challengealias')
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self._resolve_relations()
|
||||
|
||||
def _resolve_relations(self) -> None:
|
||||
if is_unset(self.p['account']):
|
||||
self.m.fail_json('You need to provide an account to create/update certificates!')
|
||||
|
||||
else:
|
||||
if len(self.existing_accounts) > 0:
|
||||
for key, values in self.existing_accounts.items():
|
||||
if values['name'] == self.p['account']:
|
||||
self.p['account'] = key
|
||||
break
|
||||
|
||||
else:
|
||||
self.m.fail_json(f"Account {self.p['account']} does not exist! {self.existing_accounts}")
|
||||
|
||||
if is_unset(self.p['validation']):
|
||||
self.m.fail_json('You need to provide the validation to create/update certificates!')
|
||||
|
||||
else:
|
||||
if len(self.existing_validations) > 0:
|
||||
for key, values in self.existing_validations.items():
|
||||
if values['name'] == self.p['validation']:
|
||||
self.p['validation'] = key
|
||||
break
|
||||
|
||||
else:
|
||||
self.m.fail_json(f"Validation {self.p['validation']} does not exist!")
|
||||
|
||||
if not is_unset(self.p['restart_actions']):
|
||||
mapping = {
|
||||
values['name']: key
|
||||
for key, values in self.existing_actions.items()
|
||||
}
|
||||
|
||||
missing = [
|
||||
action
|
||||
for action in self.p['restart_actions']
|
||||
if action not in mapping
|
||||
]
|
||||
if any(missing):
|
||||
self.m.fail_json(f"Actions {missing.join(',')} do not exist!")
|
||||
|
||||
self.p['restart_actions'] = [
|
||||
mapping[action]
|
||||
for action in self.p['restart_actions']
|
||||
]
|
||||
|
||||
def reload(self):
|
||||
# no reload required
|
||||
pass
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'acmeclient.settings'
|
||||
API_KEY_PATH_REQ = 'acmeclient.settings'
|
||||
API_MOD = 'acmeclient'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
FIELDS_CHANGE = [
|
||||
'auto_renewal', 'challenge_port', 'tls_challenge_port', 'restart_timeout',
|
||||
'haproxy_integration', 'log_level', 'show_intro',
|
||||
]
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'auto_renewal': 'autoRenewal',
|
||||
'challenge_port': 'challengePort',
|
||||
'tls_challenge_port': 'TLSchallengePort',
|
||||
'restart_timeout': 'restartTimeout',
|
||||
'haproxy_integration': 'haproxyIntegration',
|
||||
'log_level': 'logLevel',
|
||||
'show_intro': 'showIntro',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'auto_renewal', 'haproxy_integration', 'show_intro'],
|
||||
'select': ['log_level'],
|
||||
'int': ['challenge_port', 'tls_challenge_port', 'restart_timeout'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'challenge_port': {'min': 1024, 'max': 65535},
|
||||
'tls_challenge_port': {'min': 1024, 'max': 65535},
|
||||
'restart_timeout': {'min': 0, 'max': 86400},
|
||||
}
|
||||
EXIST_ATTR = 'settings'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Validation(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add',
|
||||
'del': 'del',
|
||||
'set': 'update',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle',
|
||||
}
|
||||
API_KEY_PATH = 'acmeclient.validations.validation'
|
||||
API_MOD = 'acmeclient'
|
||||
API_CONT = 'validations'
|
||||
API_CONT_GET = 'settings'
|
||||
FIELDS_CHANGE = ['description', 'method']
|
||||
FIELDS_ALL = [
|
||||
'name',
|
||||
'http_service', 'http_opn_autodiscovery', 'http_opn_interface', 'http_opn_ipaddresses', 'http_haproxy_inject',
|
||||
'http_haproxy_frontends', 'tlsalpn_acme_autodiscovery', 'tlsalpn_acme_ipaddresses', 'tlsalpn_acme_interface',
|
||||
'dns_service', 'dns_sleep', 'dns_active24_token', 'dns_ad_key', 'dns_ali_key', 'dns_ali_secret',
|
||||
'dns_autodns_user', 'dns_autodns_password', 'dns_autodns_context', 'dns_aws_id', 'dns_aws_secret',
|
||||
'dns_azuredns_subscriptionid', 'dns_azuredns_tenantid', 'dns_azuredns_appid', 'dns_azuredns_clientsecret',
|
||||
'dns_bunny_api_key', 'dns_cf_email', 'dns_cf_key', 'dns_cf_token', 'dns_cf_account_id', 'dns_cf_zone_id',
|
||||
'dns_cloudns_auth_id', 'dns_cloudns_sub_auth_id', 'dns_cloudns_auth_password', 'dns_cx_key', 'dns_cx_secret',
|
||||
'dns_cyon_user', 'dns_cyon_password', 'dns_ddnss_token', 'dns_dgon_key', 'dns_dnsexit_auth_user',
|
||||
'dns_dnsexit_auth_pass', 'dns_dnsexit_api', 'dns_dnshome_password', 'dns_dnshome_subdomain',
|
||||
'dns_dnsimple_token', 'dns_dnsservices_user', 'dns_dnsservices_password', 'dns_doapi_token', 'dns_do_pid',
|
||||
'dns_do_password', 'dns_domeneshop_token', 'dns_domeneshop_secret', 'dns_dp_id', 'dns_dp_key',
|
||||
'dns_duckdns_token', 'dns_dyn_customer', 'dns_dyn_user', 'dns_dyn_password', 'dns_dynu_clientid',
|
||||
'dns_dynu_secret', 'dns_freedns_user', 'dns_freedns_password', 'dns_fornex_api_key', 'dns_gandi_livedns_key',
|
||||
'dns_gandi_livedns_token', 'dns_gcloud_key', 'dns_googledomains_access_token', 'dns_googledomains_zone',
|
||||
'dns_gd_key', 'dns_gd_secret', 'dns_hostingde_server', 'dns_hostingde_apiKey', 'dns_he_user',
|
||||
'dns_he_password', 'dns_infoblox_credentials', 'dns_infoblox_server', 'dns_inwx_user', 'dns_inwx_password',
|
||||
'dns_inwx_shared_secret', 'dns_ionos_prefix', 'dns_ionos_secret', 'dns_ipv64_token', 'dns_ispconfig_user',
|
||||
'dns_ispconfig_password', 'dns_ispconfig_api', 'dns_ispconfig_insecure','dns_jd_id', 'dns_jd_region',
|
||||
'dns_jd_secret', 'dns_joker_username', 'dns_joker_password', 'dns_kinghost_username', 'dns_kinghost_password',
|
||||
'dns_knot_server', 'dns_knot_key', 'dns_limacity_apikey', 'dns_linode_v4_key', 'dns_loopia_api',
|
||||
'dns_loopia_user', 'dns_loopia_password', 'dns_lua_email', 'dns_lua_key', 'dns_miab_user', 'dns_miab_password',
|
||||
'dns_miab_server', 'dns_me_key', 'dns_me_secret', 'dns_mydnsjp_masterid', 'dns_mydnsjp_password',
|
||||
'dns_mythic_beasts_key', 'dns_mythic_beasts_secret', 'dns_namecheap_user', 'dns_namecheap_api',
|
||||
'dns_namecheap_sourceip', 'dns_namecom_user', 'dns_namecom_token', 'dns_namesilo_key', 'dns_nederhost_key',
|
||||
'dns_netcup_cid', 'dns_netcup_key', 'dns_netcup_pw', 'dns_njalla_token', 'dns_nsone_key',
|
||||
'dns_nsupdate_server', 'dns_nsupdate_zone', 'dns_nsupdate_key', 'dns_oci_cli_user', 'dns_oci_cli_tenancy',
|
||||
'dns_oci_cli_region', 'dns_oci_cli_key', 'dns_online_key', 'dns_opnsense_host', 'dns_opnsense_port',
|
||||
'dns_opnsense_key', 'dns_opnsense_token', 'dns_opnsense_insecure', 'dns_ovh_app_key', 'dns_ovh_app_secret',
|
||||
'dns_ovh_consumer_key', 'dns_ovh_endpoint', 'dns_pleskxml_user', 'dns_pleskxml_pass', 'dns_pleskxml_uri',
|
||||
'dns_pdns_url', 'dns_pdns_serverid', 'dns_pdns_token', 'dns_porkbun_key', 'dns_porkbun_secret', 'dns_sl_key',
|
||||
'dns_selfhost_user', 'dns_selfhost_password', 'dns_selfhost_map', 'dns_servercow_username',
|
||||
'dns_servercow_password', 'dns_simply_api_key', 'dns_simply_account_name', 'dns_transip_username',
|
||||
'dns_transip_key', 'dns_udr_user', 'dns_udr_password', 'dns_uno_key', 'dns_uno_user', 'dns_vscale_key',
|
||||
'dns_vultr_key', 'dns_yandex_token', 'dns_zilore_key', 'dns_zm_key', 'dns_gdnsdk_user', 'dns_gdnsdk_password',
|
||||
'dns_acmedns_user', 'dns_acmedns_password', 'dns_acmedns_subdomain', 'dns_acmedns_updateurl',
|
||||
'dns_acmedns_baseurl', 'dns_acmeproxy_endpoint', 'dns_acmeproxy_username', 'dns_acmeproxy_password',
|
||||
'dns_variomedia_key', 'dns_schlundtech_user', 'dns_schlundtech_password', 'dns_easydns_apitoken',
|
||||
'dns_easydns_apikey', 'dns_euserv_user', 'dns_euserv_password', 'dns_leaseweb_key', 'dns_cn_user',
|
||||
'dns_cn_password', 'dns_arvan_token', 'dns_artfiles_username', 'dns_artfiles_password', 'dns_hetzner_token',
|
||||
'dns_hexonet_login', 'dns_hexonet_password', 'dns_1984hosting_user', 'dns_1984hosting_password',
|
||||
'dns_kas_login', 'dns_kas_authdata', 'dns_kas_authtype', 'dns_desec_token', 'dns_desec_name',
|
||||
'dns_infomaniak_token', 'dns_zone_username', 'dns_zone_key', 'dns_dynv6_token', 'dns_cpanel_user',
|
||||
'dns_cpanel_token', 'dns_cpanel_hostname', 'dns_regru_username', 'dns_regru_password', 'dns_nic_username',
|
||||
'dns_nic_password', 'dns_nic_client', 'dns_nic_secret', 'dns_world4you_username', 'dns_world4you_password',
|
||||
'dns_aurora_key', 'dns_aurora_secret', 'dns_conoha_user', 'dns_conoha_password', 'dns_conoha_tenantid',
|
||||
'dns_conoha_idapi', 'dns_constellix_key', 'dns_constellix_secret', 'dns_exoscale_key', 'dns_exoscale_secret',
|
||||
'dns_internetbs_key', 'dns_internetbs_password', 'dns_pointhq_key', 'dns_pointhq_email', 'dns_rackspace_user',
|
||||
'dns_rackspace_key', 'dns_rage4_token', 'dns_rage4_user',
|
||||
]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'http_haproxy_inject': 'http_haproxyInject',
|
||||
'http_haproxy_frontends': 'http_haproxyFrontends',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'enabled', 'http_opn_autodiscovery', 'http_haproxy_inject', 'tlsalpn_acme_autodiscovery',
|
||||
'dns_opnsense_insecure', 'dns_ispconfig_insecure',
|
||||
],
|
||||
'list': ['http_opn_ipaddresses', 'http_haproxy_frontends', 'tlsalpn_acme_ipaddresses'],
|
||||
'select': [
|
||||
'method', 'http_service', 'http_opn_interface', 'tlsalpn_acme_interface', 'dns_service',
|
||||
'dns_kas_authtype',
|
||||
],
|
||||
}
|
||||
EXIST_ATTR = 'validation'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.validation = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['method']):
|
||||
self.m.fail_json('You need to provide method to create/update validations!')
|
||||
|
||||
if self.p['method'] == 'http01':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['http_service']
|
||||
if self.p['http_service'] == 'opnsense':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + [
|
||||
field
|
||||
for field in self.FIELDS_ALL
|
||||
if field.startswith('http_opn')
|
||||
]
|
||||
|
||||
else:
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + [
|
||||
field
|
||||
for field in self.FIELDS_ALL
|
||||
if field.startswith('http_haproxy')
|
||||
]
|
||||
|
||||
elif self.p['method'] == 'tlsalpn01':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + [
|
||||
field
|
||||
for field in self.FIELDS_ALL
|
||||
if field.startswith('tlsalpn_')
|
||||
]
|
||||
|
||||
elif self.p['method'] == 'dns01':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['dns_service'] + [
|
||||
field
|
||||
for field in self.FIELDS_ALL
|
||||
if field.startswith(self.p['dns_service'])
|
||||
]
|
||||
|
||||
self._base_check()
|
||||
|
||||
def reload(self):
|
||||
# no reload required
|
||||
pass
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import \
|
||||
ModuleSoftError
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.alias import \
|
||||
validate_values, filter_builtin_alias, build_updatefreq
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_simple_existing, simplify_translate, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Alias(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add_item',
|
||||
'del': 'del_item',
|
||||
'set': 'set_item',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleItem',
|
||||
}
|
||||
API_KEY_PATH = 'alias.aliases.alias'
|
||||
API_MOD = 'firewall'
|
||||
API_CONT = 'alias'
|
||||
FIELDS_CHANGE = ['content', 'description']
|
||||
FIELDS_ALL = ['name', 'type', 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_ALL.extend(['updatefreq_days', 'interface', 'path_expression'])
|
||||
FIELDS_TRANSLATE = {
|
||||
'updatefreq_days': 'updatefreq',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['type', 'interface'],
|
||||
}
|
||||
EXIST_ATTR = 'alias'
|
||||
JOIN_CHAR = '\n'
|
||||
TIMEOUT = 20.0
|
||||
MAX_ALIAS_LEN = 32
|
||||
|
||||
def __init__(
|
||||
self, module: AnsibleModule, result: dict, multi: dict = None,
|
||||
session: Session = None, fail: dict = None,
|
||||
):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail, multi=multi)
|
||||
self.alias = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['type'] == 'urltable':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['updatefreq_days']
|
||||
self.p['updatefreq_days'] = build_updatefreq(self.p['updatefreq_days'], default=True)
|
||||
|
||||
if self.p['type'] == 'urljson':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['updatefreq_days', 'path_expression']
|
||||
self.p['updatefreq_days'] = build_updatefreq(self.p['updatefreq_days'], default=True)
|
||||
|
||||
if self.p['type'] == 'dynipv6host':
|
||||
if is_unset(self.p['interface']):
|
||||
self.m.fail_json('You need to provide an interface to create a dynipv6host alias!')
|
||||
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['interface']
|
||||
|
||||
if len(self.p['name']) > self.MAX_ALIAS_LEN:
|
||||
self._error(
|
||||
f"Alias name '{self.p['name']}' is invalid - "
|
||||
f"must be shorter than {self.MAX_ALIAS_LEN} characters",
|
||||
)
|
||||
|
||||
self.b.find(match_fields=[self.FIELD_ID])
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
validate_values(error_func=self._error, cnf=self.p, existing_entries=self.existing_entries)
|
||||
|
||||
self._base_check()
|
||||
|
||||
def simplify_existing(self, alias: dict) -> dict:
|
||||
simple = {}
|
||||
|
||||
if isinstance(alias['content'], dict):
|
||||
simple['content'] = [item for item in alias['content'].keys() if item != '']
|
||||
|
||||
else:
|
||||
# if function is re-applied
|
||||
return alias
|
||||
|
||||
simple = {
|
||||
**simplify_translate(
|
||||
existing=alias,
|
||||
typing=self.FIELDS_TYPING,
|
||||
translate=self.FIELDS_TRANSLATE,
|
||||
),
|
||||
**simple,
|
||||
}
|
||||
|
||||
if simple['type'] in ['urltable', 'urljson']:
|
||||
simple['updatefreq_days'] = build_updatefreq(alias['updatefreq'], default=False)
|
||||
|
||||
return simple
|
||||
|
||||
def update(self) -> None:
|
||||
# checking if alias changed
|
||||
if self.alias['type'] == self.p['type']:
|
||||
self.b.update()
|
||||
|
||||
else:
|
||||
self.r['changed'] = True
|
||||
self._error(
|
||||
msg=f"Unable to update alias '{self.p[self.FIELD_ID]}' - it is not of the same type! "
|
||||
f"You need to delete the current one first!",
|
||||
verification=False,
|
||||
)
|
||||
|
||||
def delete(self) -> None:
|
||||
response = self.b.delete()
|
||||
|
||||
if 'in_use' in response:
|
||||
self._error(
|
||||
msg=f"Unable to delete alias '{self.p[self.FIELD_ID]}' as it is currently referenced!",
|
||||
verification=False,
|
||||
)
|
||||
|
||||
def _error(self, msg: str, verification: bool = True) -> None:
|
||||
if (verification and self.fail_verify) or (not verification and self.fail_process):
|
||||
self.m.fail_json(msg)
|
||||
|
||||
else:
|
||||
self.m.warn(msg)
|
||||
raise ModuleSoftError
|
||||
|
||||
def get_existing(self) -> list:
|
||||
return filter_builtin_alias(
|
||||
get_simple_existing(
|
||||
entries=self.b.search(),
|
||||
simplify_func=self.simplify_existing,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip_or_network, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Acl(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addAcl',
|
||||
'del': 'delAcl',
|
||||
'set': 'setAcl',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleAcl',
|
||||
}
|
||||
API_KEY_PATH = 'acl.acls.acl'
|
||||
API_MOD = 'bind'
|
||||
API_CONT = 'acl'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['networks']
|
||||
FIELDS_ALL = ['enabled', FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'list': ['networks'],
|
||||
}
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$'
|
||||
}
|
||||
EXIST_ATTR = 'acl'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.acl = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['networks']):
|
||||
self.m.fail_json('You need to provide at networks to create an ACL!')
|
||||
|
||||
for net in self.p['networks']:
|
||||
if not is_ip_or_network(net):
|
||||
self.m.fail_json(
|
||||
f"It seems you provided an invalid network: '{net}'"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class Blocklist(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsbl'
|
||||
API_MOD = 'bind'
|
||||
API_CONT = 'dnsbl'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'safe_google', 'safe_duckduckgo', 'safe_youtube', 'safe_bing',
|
||||
'exclude', 'block', 'enabled',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'block': 'type',
|
||||
'exclude': 'whitelists',
|
||||
'safe_google': 'forcesafegoogle',
|
||||
'safe_duckduckgo': 'forcesafeduckduckgo',
|
||||
'safe_youtube': 'forcesafeyoutube',
|
||||
'safe_bing': 'forcestrictbing',
|
||||
}
|
||||
FIELDS_BOOL_INVERT = ['ipv6', 'prefetch']
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'safe_google', 'safe_duckduckgo', 'safe_youtube', 'safe_bing', 'enabled',
|
||||
],
|
||||
'list': ['exclude', 'block'],
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_selected
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Domain(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addPrimaryDomain',
|
||||
'del': 'delDomain',
|
||||
'set': 'setDomain',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleDomain',
|
||||
}
|
||||
API_KEY_PATH = 'domain.domains.domain'
|
||||
API_MOD = 'bind'
|
||||
API_CONT = 'domain'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'mode', 'primary', 'transfer_key_algo', 'transfer_key_name', 'transfer_key',
|
||||
'allow_notify', 'transfer_acl', 'query_acl', 'ttl', 'refresh', 'retry',
|
||||
'expire', 'negative', 'admin_mail', 'server',
|
||||
# 'serial',
|
||||
]
|
||||
FIELDS_ALL = ['enabled', FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'name': 'domainname',
|
||||
'mode': 'type',
|
||||
'primary': 'primaryip',
|
||||
'transfer_key_algo': 'transferkeyalgo',
|
||||
'transfer_key_name': 'transferkeyname',
|
||||
'transfer_key': 'transferkey',
|
||||
'allow_notify': 'allownotifysecondary',
|
||||
'transfer_acl': 'allowtransfer',
|
||||
'query_acl': 'allowquery',
|
||||
'admin_mail': 'mailadmin',
|
||||
'server': 'dnsserver',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'list': ['primary', 'allow_notify', 'transfer_acl', 'query_acl'],
|
||||
'select': ['mode', 'transfer_key_algo'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'ttl': {'min': 60, 'max': 86400},
|
||||
'refresh': {'min': 60, 'max': 86400},
|
||||
'retry': {'min': 60, 'max': 86400},
|
||||
'expire': {'min': 60, 'max': 10000000},
|
||||
'negative': {'min': 60, 'max': 86400},
|
||||
}
|
||||
EXIST_ATTR = 'domain'
|
||||
# FIELDS_DIFF_EXCLUDE = ['serial']
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.domain = {}
|
||||
self.existing_acls = None
|
||||
self.existing_records = None
|
||||
self.acls_needed = False
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
for field in ['allow_notify', 'primary']:
|
||||
for ip in self.p[field]:
|
||||
if not is_ip(ip, ignore_empty=True):
|
||||
self.m.fail_json(
|
||||
f"It seems you provided an invalid IP address as '{field}': '{is_ip}'"
|
||||
)
|
||||
|
||||
if self.p['mode'] != 'primary':
|
||||
self.CMDS['add'] = 'addSecondaryDomain'
|
||||
|
||||
if not is_unset(self.p['query_acl']) or not is_unset(self.p['transfer_acl']):
|
||||
self.acls_needed = True
|
||||
self._search_acls()
|
||||
|
||||
self.b.find(match_fields=[self.FIELD_ID])
|
||||
|
||||
if self.exists:
|
||||
if self.p['state'] != 'present':
|
||||
# checking if domain has any record left before removing it; plugin seems to lack validation
|
||||
self._search_records()
|
||||
|
||||
if self.existing_records is not None and len(self.existing_records) > 0:
|
||||
for record in self.existing_records.values():
|
||||
if get_selected(record['domain']) == self.domain['uuid']:
|
||||
self.m.fail_json(
|
||||
f"Unable to remove domain '{self.domain['name']}' - it has at least "
|
||||
f"one existing record: '{get_selected(record['type'])}: "
|
||||
f"{record['name']}.{self.domain['name']}'"
|
||||
)
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
if self.acls_needed:
|
||||
self.b.find_multiple_links(
|
||||
field='query_acl',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='transfer_acl',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
def _search_acls(self) -> None:
|
||||
self.existing_acls = self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': self.CMDS['search'], 'controller': 'acl'}
|
||||
})['acl']['acls']['acl']
|
||||
|
||||
def _search_records(self) -> None:
|
||||
# to check if domain is still in use
|
||||
self.existing_records = self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': self.CMDS['search'], 'controller': 'record'}
|
||||
})['record']['records']['record']
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
simplify_translate
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'general'
|
||||
API_MOD = 'bind'
|
||||
API_CONT = 'general'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'ipv6', 'response_policy_zones', 'port', 'listen_ipv4', 'listen_ipv6', 'query_acl',
|
||||
'query_source_ipv4', 'query_source_ipv6', 'transfer_source_ipv4', 'transfer_source_ipv6',
|
||||
'forwarders', 'filter_aaaa_v4', 'filter_aaaa_v6', 'filter_aaaa_acl', 'log_size',
|
||||
'cache_size', 'recursion_acl', 'transfer_acl', 'dnssec_validation', 'hide_hostname',
|
||||
'hide_version', 'prefetch', 'ratelimit', 'ratelimit_count', 'ratelimit_except',
|
||||
'enabled'
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'ipv6': 'disablev6',
|
||||
'response_policy_zones': 'enablerpz',
|
||||
'listen_ipv4': 'listenv4',
|
||||
'listen_ipv6': 'listenv6',
|
||||
'query_source_ipv4': 'querysource',
|
||||
'query_source_ipv6': 'querysourcev6',
|
||||
'transfer_source_ipv4': 'transfersource',
|
||||
'transfer_source_ipv6': 'transfersourcev6',
|
||||
'filter_aaaa_v4': 'filteraaaav4',
|
||||
'filter_aaaa_v6': 'filteraaaav6',
|
||||
'filter_aaaa_acl': 'filteraaaaacl',
|
||||
'log_size': 'logsize',
|
||||
'cache_size': 'maxcachesize',
|
||||
'recursion_acl': 'recursion',
|
||||
'transfer_acl': 'allowtransfer',
|
||||
'query_acl': 'allowquery',
|
||||
'dnssec_validation': 'dnssecvalidation',
|
||||
'hide_hostname': 'hidehostname',
|
||||
'hide_version': 'hideversion',
|
||||
'prefetch': 'disableprefetch',
|
||||
'ratelimit': 'enableratelimiting',
|
||||
'ratelimit_count': 'ratelimitcount',
|
||||
'ratelimit_except': 'ratelimitexcept',
|
||||
}
|
||||
FIELDS_BOOL_INVERT = ['ipv6', 'prefetch']
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'ipv6', 'response_policy_zones', 'filter_aaaa_v4', 'filter_aaaa_v6', 'hide_hostname',
|
||||
'hide_version', 'prefetch', 'ratelimit', 'enabled',
|
||||
],
|
||||
'list': [
|
||||
'ratelimit_except', 'filter_aaaa_acl', 'forwarders', 'listen_ipv6', 'listen_ipv4',
|
||||
'transfer_acl', 'query_acl', 'recursion_acl',
|
||||
],
|
||||
'select': ['dnssec_validation'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'ratelimit_count': {'min': 1, 'max': 1000},
|
||||
'cache_size': {'min': 1, 'max': 99},
|
||||
'log_size': {'min': 1, 'max': 1000},
|
||||
'port': {'min': 1, 'max': 65535},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.existing_acls = None
|
||||
self.acls_needed = False
|
||||
|
||||
def check(self) -> None:
|
||||
self._check_validators()
|
||||
|
||||
for field in [
|
||||
'listen_ipv4', 'query_source_ipv4', 'transfer_source_ipv4',
|
||||
'listen_ipv6', 'query_source_ipv6', 'transfer_source_ipv6',
|
||||
]:
|
||||
if isinstance(self.p[field], list):
|
||||
for ip in self.p[field]:
|
||||
if not is_ip(ip, ignore_empty=True):
|
||||
self.m.fail_json(
|
||||
f"It seems you provided an invalid IP address as '{field}': '{ip}'"
|
||||
)
|
||||
|
||||
if is_unset(self.p[field]):
|
||||
self.m.fail_json(
|
||||
f"You need to supply at least one value as '{field}'! "
|
||||
'Leave it empty to only use localhost.'
|
||||
)
|
||||
|
||||
else:
|
||||
ip = self.p[field]
|
||||
if not is_ip(ip, ignore_empty=True):
|
||||
self.m.fail_json(
|
||||
f"It seems you provided an invalid IP address as '{field}': '{ip}'"
|
||||
)
|
||||
|
||||
if not is_unset(self.p['recursion_acl']) or len(self.p['transfer_acl']) > 0 or len(self.p['query_acl']) > 0:
|
||||
# to save time on call if not needed
|
||||
self.acls_needed = True
|
||||
|
||||
self.settings = self.get_existing()
|
||||
|
||||
if self.acls_needed:
|
||||
self.b.find_multiple_links(
|
||||
field='recursion_acl',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='transfer_acl',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='query_acl',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
|
||||
self._build_diff()
|
||||
|
||||
def get_existing(self) -> dict:
|
||||
if self.acls_needed:
|
||||
self.existing_acls = self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': self.CMDS['search'], 'controller': 'acl'}
|
||||
})['acl']['acls']['acl']
|
||||
|
||||
return simplify_translate(
|
||||
existing=self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': self.CMDS['search']}
|
||||
})[self.API_KEY_PATH],
|
||||
translate=self.FIELDS_TRANSLATE,
|
||||
typing=self.FIELDS_TYPING,
|
||||
bool_invert=self.FIELDS_BOOL_INVERT,
|
||||
)
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.handler import \
|
||||
ModuleSoftError
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_multiple_matching
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip4, is_ip6, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.main.bind_domain import Domain
|
||||
|
||||
|
||||
class Record(BaseModule):
|
||||
MULTI_DIFF_KEY = 'name'
|
||||
CMDS = {
|
||||
'add': 'addRecord',
|
||||
'del': 'delRecord',
|
||||
'set': 'setRecord',
|
||||
'search': 'searchRecord',
|
||||
'detail': 'getRecord',
|
||||
'toggle': 'toggleRecord',
|
||||
}
|
||||
API_KEY_PATH = 'record'
|
||||
API_MOD = 'bind'
|
||||
API_CONT = 'record'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['value']
|
||||
FIELDS_ALL = ['domain', 'name', 'type', 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['type', 'domain'],
|
||||
}
|
||||
FIELDS_RR_MATCH = ['domain', 'name', 'type', 'value']
|
||||
EXIST_ATTR = 'record'
|
||||
|
||||
def __init__(
|
||||
self, module: AnsibleModule, result: dict, multi: dict = None,
|
||||
session: Session = None, fail: dict = None,
|
||||
):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail, multi=multi)
|
||||
self.existing = []
|
||||
self.record = {}
|
||||
self.existing_entries = None
|
||||
self.existing_domains = None
|
||||
self.existing_domain_mapping = None
|
||||
self.exists = False
|
||||
self.exists_rr = False
|
||||
|
||||
# pylint: disable=R0915
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['value']):
|
||||
self._error(
|
||||
'You need to supply a value to create the record '
|
||||
f"'{self.p['name']}.{self.p['domain']}'"
|
||||
)
|
||||
|
||||
else:
|
||||
if self.p['type'] == 'A' and not is_ip4(self.p['value']):
|
||||
self.m.fail_json(f"Value '{self.p['value']}' is not a valid IPv4-address!")
|
||||
|
||||
elif self.p['type'] == 'AAAA' and not is_ip6(self.p['value']):
|
||||
self.m.fail_json(f"Value '{self.p['value']}' is not a valid IPv6-address!")
|
||||
|
||||
# custom matching as dns round-robin allows for multiple records to match..
|
||||
self.search_call_domains()
|
||||
if self.existing_entries is None:
|
||||
self.existing_entries = self.get_existing()
|
||||
|
||||
if len(self.existing_domains) == 0:
|
||||
if self.p['state'] == 'present':
|
||||
self._error('No existing domain found! Create one before managing its records.')
|
||||
|
||||
else:
|
||||
domain_found = False
|
||||
if self.existing_domain_mapping is None:
|
||||
for uuid, dom in self.existing_domains.items():
|
||||
if dom['domainname'] == self.p['domain'] or uuid == self.p['domain']:
|
||||
self.p['domain'] = uuid
|
||||
domain_found = True
|
||||
break
|
||||
|
||||
else:
|
||||
if self.p['domain'] in self.existing_domain_mapping:
|
||||
self.p['domain'] = self.existing_domain_mapping[self.p['domain']]
|
||||
domain_found = True
|
||||
|
||||
if not domain_found:
|
||||
self._error(
|
||||
f"The provided domain '{self.p['domain']}' was not found! "
|
||||
'You may have to create it before managing its records.'
|
||||
)
|
||||
|
||||
self.existing = get_multiple_matching(
|
||||
module=self.m, existing_items=self.existing_entries,
|
||||
compare_item=self.p, match_fields=self.p['match_fields'],
|
||||
simplify_func=self.b.simplify_existing,
|
||||
)
|
||||
|
||||
self.exists_rr = len(self.existing) > 1
|
||||
self.exists = len(self.existing) == 1
|
||||
|
||||
if self.exists_rr or self.p.get('round_robin', False):
|
||||
self.r['diff']['before'] = self.existing
|
||||
|
||||
else:
|
||||
if self.exists:
|
||||
self.record = self.existing[0]
|
||||
self.r['diff']['before'] = self.record
|
||||
self.call_cnf['params'] = [self.record['uuid']]
|
||||
|
||||
self._base_check()
|
||||
|
||||
def _search_call(self) -> list:
|
||||
self.search_call_domains()
|
||||
|
||||
existing = []
|
||||
for uuid in self.existing_domains:
|
||||
existing.extend(self.b.api_search_post(
|
||||
cnf={
|
||||
'module': self.API_MOD,
|
||||
'controller': self.API_CONT,
|
||||
'command': self.CMDS['search'],
|
||||
},
|
||||
data={'domain': uuid}
|
||||
))
|
||||
|
||||
return existing
|
||||
|
||||
def search_call_domains(self):
|
||||
if self.existing_domains is not None:
|
||||
return
|
||||
|
||||
data = self.s.get(cnf={
|
||||
'module': Domain.API_MOD,
|
||||
'controller': Domain.API_CONT,
|
||||
'command': Domain.CMDS['search'],
|
||||
})
|
||||
for k in Domain.API_KEY_PATH.split('.'):
|
||||
data = data[k]
|
||||
|
||||
self.existing_domains = data
|
||||
|
||||
def _error(self, msg: str, verification: bool = True) -> None:
|
||||
if (verification and self.fail_verify) or (not verification and self.fail_process):
|
||||
self.m.fail_json(msg)
|
||||
|
||||
else:
|
||||
self.m.warn(msg)
|
||||
raise ModuleSoftError
|
||||
|
||||
def _delete_rr(self) -> None:
|
||||
self.r['diff']['after'] = {}
|
||||
|
||||
for record in self.existing:
|
||||
self.call_cnf['params'] = [record['uuid']]
|
||||
self.delete()
|
||||
|
||||
def process(self) -> None:
|
||||
if self.exists_rr or self.p['round_robin']:
|
||||
# round-robin exists
|
||||
if not self.p['round_robin']:
|
||||
if self.p['state'] == 'present':
|
||||
self._error(
|
||||
msg='Multiple records with the provided domain/type/name combination exist! '
|
||||
"To create 'round_robin' records - set the argument to 'true'. "
|
||||
"Else remove all existing records by re-calling the module with 'state=absent'",
|
||||
verification=False,
|
||||
)
|
||||
|
||||
else:
|
||||
if self.exists_rr:
|
||||
self._delete_rr()
|
||||
|
||||
else:
|
||||
self.delete()
|
||||
|
||||
else:
|
||||
if self.p['state'] == 'present':
|
||||
if not self._exists_rr():
|
||||
self._diff_rr()
|
||||
self.create()
|
||||
|
||||
else:
|
||||
self._delete_rr()
|
||||
|
||||
else:
|
||||
# single record
|
||||
self.b.process()
|
||||
|
||||
def _exists_rr(self) -> bool:
|
||||
# check if exact same record already exists if using round-robin
|
||||
for e in self.existing:
|
||||
matching = []
|
||||
for f in self.FIELDS_RR_MATCH:
|
||||
matching.append(e[f] == self.p[f])
|
||||
|
||||
if all(matching):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _diff_rr(self) -> None:
|
||||
def _key(item: dict, idx: int) -> str:
|
||||
return f"{item['type']}:{item['name']}.{item['domain']}#{idx}"
|
||||
|
||||
_before = {}
|
||||
_after = {}
|
||||
|
||||
_idx = 0
|
||||
for e in self.existing:
|
||||
_before[_key(item=e, idx=_idx)] = e
|
||||
_idx += 1
|
||||
|
||||
_new = self.b.build_diff(data=self.p)
|
||||
_after[_key(item=_new, idx=_idx)] = _new
|
||||
|
||||
self.r['diff']['after'] = {**_before, **_after}
|
||||
self.r['diff']['before'] = _before
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class CronJob(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_job',
|
||||
'del': 'del_job',
|
||||
'set': 'set_job',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle_job', # test
|
||||
}
|
||||
API_KEY_PATH = 'job.jobs.job'
|
||||
API_MOD = 'cron'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'minutes', 'hours', 'days', 'months',
|
||||
'weekdays', 'command', 'who', 'parameters'
|
||||
]
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['command'],
|
||||
'int': ['minutes', 'hours', 'days', 'months', 'weekdays'],
|
||||
}
|
||||
FIELDS_ALL = ['description', 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'cron'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.cron = {}
|
||||
self.available_commands = []
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present' and is_unset(self.p['command']):
|
||||
self.m.fail_json("You need to provide a 'command' if you want to create a cron-job!")
|
||||
|
||||
self.b.find(match_fields=[self.FIELD_ID])
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
if self.p['command'] is not None and len(self.available_commands) > 0 and \
|
||||
self.p['command'] not in self.available_commands:
|
||||
self.m.fail_json(
|
||||
'Got unsupported command! '
|
||||
f"Available ones are: {', '.join(self.available_commands)}"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
def _build_all_available_cmds(self, raw_cmds: dict):
|
||||
if len(self.available_commands) == 0:
|
||||
for cmd in raw_cmds.keys():
|
||||
if cmd not in self.available_commands:
|
||||
self.available_commands.append(cmd)
|
||||
|
||||
def _simplify_existing(self, existing: dict) -> dict:
|
||||
simple = self.b.simplify_existing(existing)
|
||||
simple.pop('origin')
|
||||
self._build_all_available_cmds(existing['command'])
|
||||
return simple
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class ControlAgent(GeneralModule):
|
||||
FIELD_ID = 'ip'
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'ctrlagent.general'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'kea'
|
||||
API_CONT = 'ctrl_agent'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'enabled', 'http_host', 'http_port'
|
||||
]
|
||||
FIELDS_ALL = [*FIELDS_CHANGE]
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'int': ['http_port'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'http_port': {'min': 1, 'max': 65535},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
||||
def check(self) -> None:
|
||||
if not is_ip(self.p['http_host']):
|
||||
self.m.fail_json('The provided IP is invalid!')
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get'
|
||||
}
|
||||
API_KEY_PATH = 'dhcpv4.general'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'kea'
|
||||
API_CONT = 'dhcpv4'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'enabled', 'interfaces', 'socket_type', 'fw_rules', 'lifetime'
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'lifetime': 'valid_lifetime',
|
||||
'fw_rules': 'fwrules',
|
||||
'socket_type': 'dhcp_socket_type',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'fw_rules'],
|
||||
'int': ['lifetime'],
|
||||
'list': ['interfaces'],
|
||||
'select': ['socket_type'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'lifetime': {'min': 0},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip, is_network, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class ReservationV4(BaseModule):
|
||||
FIELD_ID = 'ip'
|
||||
CMDS = {
|
||||
'add': 'add_reservation',
|
||||
'del': 'del_reservation',
|
||||
'set': 'set_reservation',
|
||||
'search': 'search_reservation',
|
||||
'detail': 'get_reservation',
|
||||
}
|
||||
API_KEY_PATH = 'reservation'
|
||||
API_MOD = 'kea'
|
||||
API_CONT = 'dhcpv4'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'mac', 'hostname', 'description', 'subnet'
|
||||
]
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'select': ['subnet'],
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'ip': 'ip_address',
|
||||
'mac': 'hw_address',
|
||||
}
|
||||
EXIST_ATTR = 'reservation'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.reservation = {}
|
||||
self.existing_reservations = None
|
||||
self.existing_subnets = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['mac']):
|
||||
self.m.fail_json(
|
||||
"You need to provide a 'mac' if you want to create a reservation!"
|
||||
)
|
||||
|
||||
if is_unset(self.p['subnet']) or not is_network(self.p['subnet']):
|
||||
self.m.fail_json('The provided subnet is invalid!')
|
||||
|
||||
if not is_ip(self.p['ip']):
|
||||
self.m.fail_json('The provided IP is invalid!')
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self._search_subnets()
|
||||
if not self._find_subnet():
|
||||
self.m.fail_json('Provided subnet not found!')
|
||||
|
||||
def _find_subnet(self) -> bool:
|
||||
for s in self.existing_subnets:
|
||||
if s['subnet'] == self.p['subnet']:
|
||||
self.p['subnet'] = s['uuid']
|
||||
self.reservation['subnet'] = s['uuid']
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _search_subnets(self):
|
||||
self.existing_subnets = self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': 'searchSubnet'}
|
||||
})['rows']
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_selected_list, simplify_translate
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class SubnetV4(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'add_subnet',
|
||||
'del': 'del_subnet',
|
||||
'set': 'set_subnet',
|
||||
'search': 'search_subnet',
|
||||
'detail': 'get_subnet',
|
||||
}
|
||||
API_KEY = 'subnet4'
|
||||
API_KEY_PATH = 'subnet4'
|
||||
API_MOD = 'kea'
|
||||
API_CONT = 'dhcpv4'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'subnet', 'description', 'pools', 'auto_options',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TYPING = {
|
||||
'list': ['gateway', 'dns', 'domain_search', 'ntp_servers', 'time_servers'], # 'pools',
|
||||
'bool': ['auto_options'],
|
||||
'int': ['v6_only_preferred'],
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'auto_options': 'option_data_autocollect',
|
||||
}
|
||||
API_ATTR_OPTIONS = 'option_data'
|
||||
API_FIELDS_OPTIONS = [
|
||||
'gateway', 'routes', 'dns', 'domain', 'domain_search', 'ntp_servers', 'time_servers',
|
||||
'next_server', 'tftp_server', 'tftp_file', 'v6_only_preferred',
|
||||
]
|
||||
POOL_JOIN_CHAR = '\n'
|
||||
FIELDS_TRANSLATE_SPECIAL = {
|
||||
'dns': 'domain_name_servers',
|
||||
'domain': 'domain_name',
|
||||
'gateway': 'routers',
|
||||
'routes': 'static_routes',
|
||||
'tftp_server': 'tftp_server_name',
|
||||
'tftp_file': 'boot_file_name',
|
||||
}
|
||||
EXIST_ATTR = 'subnet'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.subnet = {}
|
||||
self.existing_subnets = None
|
||||
|
||||
def _simplify_existing(self, entry: dict) -> dict:
|
||||
simple = simplify_translate(
|
||||
existing=entry,
|
||||
typing=self.FIELDS_TYPING,
|
||||
translate=self.FIELDS_TRANSLATE,
|
||||
ignore=self.API_FIELDS_OPTIONS,
|
||||
)
|
||||
|
||||
simple['pools'] = simple['pools'].split(self.POOL_JOIN_CHAR)
|
||||
if self.API_ATTR_OPTIONS in entry:
|
||||
# get/details call
|
||||
opts = entry[self.API_ATTR_OPTIONS]
|
||||
return {
|
||||
**simple,
|
||||
'dns': get_selected_list(opts[self.FIELDS_TRANSLATE_SPECIAL['dns']]),
|
||||
'domain_search': get_selected_list(opts['domain_search']),
|
||||
'gateway': get_selected_list(opts[self.FIELDS_TRANSLATE_SPECIAL['gateway']]),
|
||||
'routes': opts[self.FIELDS_TRANSLATE_SPECIAL['routes']],
|
||||
'domain': opts[self.FIELDS_TRANSLATE_SPECIAL['domain']],
|
||||
'ntp_servers': get_selected_list(opts['ntp_servers']),
|
||||
'time_servers': get_selected_list(opts['time_servers']),
|
||||
'tftp_server': opts[self.FIELDS_TRANSLATE_SPECIAL['tftp_server']],
|
||||
'tftp_file': opts[self.FIELDS_TRANSLATE_SPECIAL['tftp_file']],
|
||||
'v6_only_preferred': opts['v6_only_preferred'],
|
||||
}
|
||||
|
||||
# search-call :'(
|
||||
return {
|
||||
**simple,
|
||||
'dns': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['dns']}"],
|
||||
'domain_search': entry['option_data.domain_search'],
|
||||
'gateway': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['gateway']}"],
|
||||
'routes': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['routes']}"],
|
||||
'domain': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['domain']}"],
|
||||
'ntp_servers': entry['option_data.ntp_servers'],
|
||||
'time_servers': entry['option_data.time_servers'],
|
||||
'tftp_server': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['tftp_server']}"],
|
||||
'tftp_file': entry[f"option_data.{self.FIELDS_TRANSLATE_SPECIAL['tftp_file']}"],
|
||||
'v6_only_preferred': entry['option_data.v6_only_preferred'],
|
||||
}
|
||||
|
||||
def _build_request(self) -> dict:
|
||||
raw_request = self.b.build_request(ignore_fields=self.API_FIELDS_OPTIONS)
|
||||
|
||||
raw_request[self.API_KEY]['pools'] = self.POOL_JOIN_CHAR.join(self.p['pools'])
|
||||
raw_request[self.API_KEY][self.API_ATTR_OPTIONS] = {
|
||||
self.FIELDS_TRANSLATE_SPECIAL['dns']: self.b.RESP_JOIN_CHAR.join(self.p['dns']),
|
||||
self.FIELDS_TRANSLATE_SPECIAL['gateway']: self.b.RESP_JOIN_CHAR.join(self.p['gateway']),
|
||||
self.FIELDS_TRANSLATE_SPECIAL['routes']: self.p['routes'],
|
||||
self.FIELDS_TRANSLATE_SPECIAL['domain']: self.p['domain'],
|
||||
self.FIELDS_TRANSLATE_SPECIAL['tftp_server']: self.p['tftp_server'],
|
||||
self.FIELDS_TRANSLATE_SPECIAL['tftp_file']: self.p['tftp_file'],
|
||||
'ntp_servers': self.b.RESP_JOIN_CHAR.join(self.p['ntp_servers']),
|
||||
'time_servers': self.b.RESP_JOIN_CHAR.join(self.p['time_servers']),
|
||||
'domain_search': self.b.RESP_JOIN_CHAR.join(self.p['domain_search']),
|
||||
'v6_only_preferred': self.p['v6_only_preferred'],
|
||||
}
|
||||
|
||||
return raw_request
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class DhcRelayDestination(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add_dest',
|
||||
'del': 'del_dest',
|
||||
'set': 'set_dest',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dhcrelay.destinations'
|
||||
API_KEY_PATH_REQ = 'destination'
|
||||
API_MOD = 'dhcrelay'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['server']
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'list': ['server'],
|
||||
}
|
||||
EXIST_ATTR = 'destination'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.destination = {}
|
||||
|
||||
def check(self) -> None:
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['server']):
|
||||
self.m.fail_json("You need to provide list of 'server' to create a dhcrelay_destination!")
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class DhcRelayRelay(BaseModule):
|
||||
FIELD_ID = 'interface'
|
||||
CMDS = {
|
||||
'add': 'add_relay',
|
||||
'del': 'del_relay',
|
||||
'set': 'set_relay',
|
||||
'search': 'get',
|
||||
'toggle': 'toggle_relay',
|
||||
}
|
||||
API_KEY_PATH = 'dhcrelay.relays'
|
||||
API_KEY_PATH_REQ = 'relay'
|
||||
API_MOD = 'dhcrelay'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['destination', 'agent_info']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'select': ['interface', 'destination'],
|
||||
'bool': ['enabled', 'agent_info']
|
||||
}
|
||||
EXIST_ATTR = 'relay'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_destinations': 'dhcrelay.destinations',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.relay = {}
|
||||
self.existing_destinations = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['destination']):
|
||||
self.m.fail_json("You need to provide a 'destination' to create a dhcrelay_relay!")
|
||||
|
||||
self._base_check()
|
||||
|
||||
if not is_unset(self.p['destination']) and self.existing_destinations:
|
||||
for key, values in self.existing_destinations.items():
|
||||
if values['name'] == self.p['destination']:
|
||||
self.p['destination'] = key
|
||||
break
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = self.b.get_existing()
|
||||
for relay in existing:
|
||||
relay['destination'] = self.existing_destinations[relay['destination']]['name']
|
||||
return existing
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Boot(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_boot',
|
||||
'del': 'del_boot',
|
||||
'set': 'set_boot',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.dhcp_boot'
|
||||
API_KEY_PATH_REQ = 'boot'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['address', 'filename', 'interface', 'servername', 'tag']
|
||||
FIELDS_TYPING = {
|
||||
'select': ['interface'],
|
||||
'list': ['tag'],
|
||||
}
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'boot'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_tag': 'dnsmasq.dhcp_tags',
|
||||
'existing_interface': 'dnsmasq.interface',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.boot = {}
|
||||
self.existing_tag = {}
|
||||
self.existing_interface = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='interface',
|
||||
existing=self.existing_interface,
|
||||
existing_field_id='value',
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='tag',
|
||||
existing_field_id='tag',
|
||||
existing=self.existing_tag,
|
||||
fail_soft=True, fail=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Domain(BaseModule):
|
||||
FIELD_ID = 'domain'
|
||||
CMDS = {
|
||||
'add': 'add_domain',
|
||||
'del': 'del_domain',
|
||||
'set': 'set_domain',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.domainoverrides'
|
||||
API_KEY_PATH_REQ = 'domainoverride'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['sequence', 'ipset', 'src_ip', 'port', 'ip', 'description']
|
||||
FIELDS_TRANSLATE = {
|
||||
'description': 'descr',
|
||||
'src_ip': 'srcip',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'select': ['ipset'],
|
||||
'int': ['sequence', 'port'],
|
||||
}
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'domain'
|
||||
INT_VALIDATIONS = {
|
||||
'sequence': {'min': 1, 'max': 99999},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.domain = {}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get'
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'enabled', 'interfaces', 'regdhcp', 'regdhcpstatic', 'domain_needed', 'port', 'dnssec',
|
||||
'resolve_etc_hosts', 'dhcpfirst', 'strict_order', 'strictbind', 'forward_private_reverse', 'log_queries',
|
||||
'ident', 'regdhcpdomain', 'dns_forward_max', 'cache_size', 'local_ttl', 'resolv_system',
|
||||
'add_mac', 'add_subnet', 'add_subnet', 'dhcp_disable_interfaces', 'dhcp_fqdn', 'dhcp_domain',
|
||||
'dhcp_local', 'dhcp_lease_max', 'dhcp_authoritative', 'dhcp_reply_delay', 'dhcp_default_fw_rules',
|
||||
'dhcp_enable_ra', 'dhcp_hasync',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'enabled': 'enable',
|
||||
'interfaces': 'interface',
|
||||
'resolve_etc_hosts': 'no_hosts',
|
||||
'ident': 'no_ident',
|
||||
'resolv_system': 'no_resolv',
|
||||
'forward_private_reverse': 'no_private_reverse',
|
||||
'dhcp_disable_interfaces': ('dhcp', 'no_interface'),
|
||||
'dhcp_fqdn': ('dhcp', 'fqdn'),
|
||||
'dhcp_domain': ('dhcp', 'domain'),
|
||||
'dhcp_local': ('dhcp', 'local'),
|
||||
'dhcp_lease_max': ('dhcp', 'lease_max'),
|
||||
'dhcp_authoritative': ('dhcp', 'authoritative'),
|
||||
'dhcp_default_fw_rules': ('dhcp', 'default_fw_rules'),
|
||||
'dhcp_reply_delay': ('dhcp', 'reply_delay'),
|
||||
'dhcp_enable_ra': ('dhcp', 'enable_ra'),
|
||||
'dhcp_hasync': ('dhcp', 'nosync'),
|
||||
}
|
||||
FIELDS_BOOL_INVERT = ['resolve_etc_hosts', 'ident', 'forward_private_reverse', 'dhcp_hasync']
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'enabled','regdhcp','regdhcpstatic','domain_needed', 'dnssec', 'resolve_etc_hosts', 'dhcpfirst',
|
||||
'strict_order', 'strictbind', 'forward_private_reverse', 'log_queries', 'ident', 'resolv_system',
|
||||
'add_subnet', 'add_subnet', 'dhcp_fqdn', 'dhcp_local', 'dhcp_authoritative', 'dhcp_default_fw_rules',
|
||||
'dhcp_enable_ra', 'dhcp_hasync',
|
||||
],
|
||||
'int': ['port', 'dns_forward_max', 'cache_size', 'local_ttl', 'dhcp_lease_max', 'dhcp_reply_delay'],
|
||||
'list': ['interfaces', 'dhcp_disable_interfaces'],
|
||||
'str': ['regdhcpdomain'],
|
||||
'select': ['add_mac'],
|
||||
}
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_interfaces': 'dnsmasq.interface',
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'port': {'min': 0, 'max': 65535},
|
||||
'dns_forward_max': {'min': 0},
|
||||
'cache_size': {'min': 0},
|
||||
'local_ttl': {'min': 0},
|
||||
'dhcp_lease_max': {'min': 0},
|
||||
'dhcp_reply_delay': {'min': 0, 'max': 60},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.existing_interfaces = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
self.b.find_multiple_links(
|
||||
field='interfaces',
|
||||
existing_field_id='value',
|
||||
existing=self.existing_interfaces,
|
||||
fail_soft=False, fail=False,
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='dhcp_disable_interfaces',
|
||||
existing_field_id='value',
|
||||
existing=self.existing_interfaces,
|
||||
fail_soft=False, fail=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_valid_domain
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Host(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_host',
|
||||
'del': 'del_host',
|
||||
'set': 'set_host',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.hosts'
|
||||
API_KEY_PATH_REQ = 'host'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'host', 'domain', 'local', 'ip', 'aliases', 'cnames', 'client_id', 'hardware_addr', 'lease_time', 'ignore',
|
||||
'set_tag', 'comments',
|
||||
]
|
||||
FIELDS_TRANSLATE = {
|
||||
'description': 'descr',
|
||||
'hardware_addr': 'hwaddr',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'list': ['ip', 'aliases', 'cnames', 'hardware_addr'],
|
||||
'select': ['set_tag'],
|
||||
'bool': ['local', 'ignore'],
|
||||
'int': ['lease_time'],
|
||||
}
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'host'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_tag': 'dnsmasq.dhcp_tags',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.host = {}
|
||||
self.existing_tag = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if not is_unset(self.p['domain']) and not is_valid_domain(self.p['domain']):
|
||||
self.m.fail_json(
|
||||
f"Value of domain '{self.p['domain']}' is not a valid domain-name!"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='set_tag',
|
||||
existing=self.existing_tag,
|
||||
existing_field_id='tag',
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Option(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_option',
|
||||
'del': 'del_option',
|
||||
'set': 'set_option',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.dhcp_options'
|
||||
API_KEY_PATH_REQ = 'option'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['type', 'option', 'option6', 'interface', 'tag', 'set_tag', 'value', 'force']
|
||||
FIELDS_TYPING = {
|
||||
'select': ['type', 'option', 'option6', 'interface', 'set_tag'],
|
||||
'list': ['tag'],
|
||||
'bool': ['force'],
|
||||
}
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'option'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_tag': 'dnsmasq.dhcp_tags',
|
||||
'existing_interface': 'dnsmasq.interface',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.option = {}
|
||||
self.existing_tag = {}
|
||||
self.existing_interface = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present' and self.p['type'] == 'match':
|
||||
if is_unset(self.p['set_tag']):
|
||||
self.m.fail_json("When type is 'match', a set_tag must be set.")
|
||||
if not is_unset(self.p['interface']):
|
||||
self.m.fail_json("When type is 'match', no internet can be set.")
|
||||
if not is_unset(self.p['tag']):
|
||||
self.m.fail_json("When type is 'match', no tag can be set.")
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='interface',
|
||||
existing=self.existing_interface,
|
||||
existing_field_id='value',
|
||||
)
|
||||
self.b.find_single_link(
|
||||
field='set_tag',
|
||||
existing=self.existing_tag,
|
||||
existing_field_id='tag',
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='tag',
|
||||
existing_field_id='tag',
|
||||
existing=self.existing_tag,
|
||||
fail_soft=True, fail=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip6
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Range(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_range',
|
||||
'del': 'del_range',
|
||||
'set': 'set_range',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.dhcp_ranges'
|
||||
API_KEY_PATH_REQ = 'range'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'interface', 'set_tag', 'start_addr', 'end_addr', 'subnet_mask', 'constructor', 'mode',
|
||||
'prefix_len', 'lease_time', 'domain_type', 'domain', 'sync', 'ra_mode', 'ra_priority',
|
||||
'ra_mtu', 'ra_interval', 'ra_router_lifetime'
|
||||
]
|
||||
FIELDS_BOOL_INVERT = ['sync']
|
||||
FIELDS_TRANSLATE = {
|
||||
'sync': 'nosync',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'select': ['interface', 'set_tag', 'constructor', 'mode', 'domain_type', 'ra_priority'],
|
||||
'list': ['ra_mode'],
|
||||
'int': ['prefix_len', 'lease_time', 'ra_mtu', 'ra_interval', 'ra_router_lifetime'],
|
||||
'bool': ['sync'],
|
||||
}
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'option'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_tag': 'dnsmasq.dhcp_tags',
|
||||
'existing_interface': 'dnsmasq.interface',
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'prefix_len': {'min': 1, 'max': 64},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.option = {}
|
||||
self.existing_tag = {}
|
||||
self.existing_interface = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_ip6(self.p['start_addr']):
|
||||
pass
|
||||
else:
|
||||
self.p['prefix_len'] = ''
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='interface',
|
||||
existing=self.existing_interface,
|
||||
existing_field_id='value',
|
||||
)
|
||||
self.b.find_single_link(
|
||||
field='constructor',
|
||||
existing=self.existing_interface,
|
||||
existing_field_id='value',
|
||||
)
|
||||
self.b.find_single_link(
|
||||
field='set_tag',
|
||||
existing=self.existing_tag,
|
||||
existing_field_id='tag',
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Tag(BaseModule):
|
||||
FIELD_ID = 'tag'
|
||||
CMDS = {
|
||||
'add': 'add_tag',
|
||||
'del': 'del_tag',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'dnsmasq.dhcp_tags'
|
||||
API_KEY_PATH_REQ = 'tag'
|
||||
API_MOD = 'dnsmasq'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = []
|
||||
FIELDS_TYPING = {}
|
||||
FIELDS_ALL = ['tag']
|
||||
EXIST_ATTR = 'tag'
|
||||
STR_VALIDATIONS = {
|
||||
'tag': r'^[0-9a-zA-Z]{1,1024}$'
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.tag = {}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip_or_network
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Neighbor(BaseModule):
|
||||
FIELD_ID = 'ip'
|
||||
CMDS = {
|
||||
'add': 'addNeighbor',
|
||||
'del': 'delNeighbor',
|
||||
'set': 'setNeighbor',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleNeighbor',
|
||||
}
|
||||
API_KEY_PATH = 'bfd.neighbors.neighbor'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bfd'
|
||||
FIELDS_CHANGE = ['description']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'neighbor'
|
||||
FIELDS_TRANSLATE = {
|
||||
'ip': 'address',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.neighbor = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if not is_ip_or_network(self.p[self.FIELD_ID]):
|
||||
self.m.fail_json(f"Value '{self.p[self.FIELD_ID]}' is not a valid IP address!")
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class AsPath(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'addAspath',
|
||||
'del': 'delAspath',
|
||||
'set': 'setAspath',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleAspath',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.aspaths.aspath'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['number', 'action', 'as_pattern']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'as_pattern': 'as',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'number': {'min': 10, 'max': 99},
|
||||
}
|
||||
EXIST_ATTR = 'as_path'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.as_path = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['number']) or is_unset(self.p['as_pattern']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a BGP as-path you need to provide a number, '
|
||||
'as_pattern and action!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Community(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'addCommunitylist',
|
||||
'del': 'delCommunitylist',
|
||||
'set': 'setCommunitylist',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleCommunitylist',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.communitylists.communitylist'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['number', 'seq', 'action', 'community']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'seq': 'seqnumber',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'number': {'min': 1, 'max': 500},
|
||||
'seq': {'min': 10, 'max': 99},
|
||||
}
|
||||
EXIST_ATTR = 'community_list'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.community_list = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['number']) or is_unset(self.p['seq']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a BGP community-list you need to provide a number, '
|
||||
'sequence-number and action!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'bgp'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'as_number', 'id', 'graceful', 'enabled', 'networks', 'distance', 'log_neighbor_changes',
|
||||
'network_import_check',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'as_number': 'asnumber',
|
||||
'id': 'routerid',
|
||||
'log_neighbor_changes': 'logneighborchanges',
|
||||
'network_import_check': 'networkimportcheck',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'graceful', 'network_import_check', 'log_neighbor_changes'],
|
||||
'list': ['networks'],
|
||||
'int': ['distance', 'as_number'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'as_number': {'min': 1, 'max': 4294967295},
|
||||
'distance': {'min': 1, 'max': 255},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Neighbor(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addNeighbor',
|
||||
'del': 'delNeighbor',
|
||||
'set': 'setNeighbor',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleNeighbor',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.neighbors.neighbor'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'ip', 'as_number', 'password', 'weight', 'local_ip', 'source_int',
|
||||
'ipv6_link_local_int', 'next_hop_self', 'next_hop_self_all',
|
||||
'multi_hop', 'multi_protocol', 'rrclient', 'bfd', 'send_default_route',
|
||||
'as_override', 'disable_connected_check', 'keepalive', 'hold_down',
|
||||
'connect_timer', 'description', 'prefix_list_in', 'prefix_list_out',
|
||||
'route_map_in', 'route_map_out', 'remote_as_mode',
|
||||
]
|
||||
FIELDS_DIFF_NO_LOG = ['password']
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'as_number': 'remoteas',
|
||||
'ip': 'address',
|
||||
'local_ip': 'localip',
|
||||
'source_int': 'updatesource',
|
||||
'ipv6_link_local_int': 'linklocalinterface',
|
||||
'next_hop_self': 'nexthopself',
|
||||
'next_hop_self_all': 'nexthopselfall',
|
||||
'multi_hop': 'multihop',
|
||||
'multi_protocol': 'multiprotocol',
|
||||
'hold_down': 'holddown',
|
||||
'connect_timer': 'connecttimer',
|
||||
'send_default_route': 'defaultoriginate',
|
||||
'as_override': 'asoverride',
|
||||
'prefix_list_in': 'linkedPrefixlistIn',
|
||||
'prefix_list_out': 'linkedPrefixlistOut',
|
||||
'route_map_in': 'linkedRoutemapIn',
|
||||
'route_map_out': 'linkedRoutemapOut',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'next_hop_self', 'next_hop_self_all', 'multi_hop', 'multi_protocol', 'enabled',
|
||||
'rrclient', 'bfd', 'send_default_route', 'as_override', 'disable_connected_check',
|
||||
],
|
||||
'select': [
|
||||
'source_int', 'ipv6_link_local_int', 'prefix_list_in', 'prefix_list_out',
|
||||
'route_map_in', 'route_map_out', 'remote_as_mode',
|
||||
],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'as_number': {'min': 1, 'max': 4294967295},
|
||||
'weight': {'min': 0, 'max': 65535},
|
||||
'keepalive': {'min': 1, 'max': 1000},
|
||||
'hold_down': {'min': 3, 'max': 3000},
|
||||
'connect_timer': {'min': 1, 'max': 65000},
|
||||
}
|
||||
EXIST_ATTR = 'neighbor'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'bgp.prefixlists.prefixlist',
|
||||
'existing_maps': 'bgp.routemaps.routemap',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.neighbor = {}
|
||||
self.existing_prefixes = None
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['ip']):
|
||||
self.m.fail_json('To create a BGP neighbor you need to provide its peer-ip!')
|
||||
|
||||
if not is_ip(self.p['ip']):
|
||||
self.m.fail_json(f"Provided peer IP '{self.p['ip']}' is not a valid IP-Address!")
|
||||
|
||||
if not is_unset(self.p['local_ip']) and not is_ip(self.p['local_ip']):
|
||||
self.m.fail_json(f"Provided source IP '{self.p['local_ip']}' is not a valid IP-Address!")
|
||||
|
||||
self._base_check()
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
links = {
|
||||
'prefix-list-in': {
|
||||
'found': False,
|
||||
'existing': self.existing_prefixes,
|
||||
'match_fields': {'name': 'prefix_list_in'}
|
||||
},
|
||||
'prefix-list-out': {
|
||||
'found': False,
|
||||
'existing': self.existing_prefixes,
|
||||
'match_fields': {'name': 'prefix_list_out'}
|
||||
},
|
||||
'route-map-in': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map_in'}
|
||||
},
|
||||
'route-map-out': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map_out'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, values in links.items():
|
||||
value_name = values['match_fields']['name']
|
||||
provided = not is_unset(self.p[value_name])
|
||||
seq_uuid_mapping = {}
|
||||
|
||||
if not provided:
|
||||
continue
|
||||
|
||||
if len(values['existing']) > 0:
|
||||
for uuid, entry in values['existing'].items():
|
||||
matching = []
|
||||
|
||||
for api_field, ans_field in values['match_fields'].items():
|
||||
if not is_unset(self.p[ans_field]):
|
||||
matching.append(str(entry[api_field]) == str(self.p[ans_field]))
|
||||
|
||||
if all(matching):
|
||||
self.p[value_name] = uuid
|
||||
values['found'] = True
|
||||
|
||||
if 'seqnumber' in entry:
|
||||
seq_uuid_mapping[int(entry['seqnumber'])] = uuid
|
||||
|
||||
if not values['found']:
|
||||
self.m.fail_json(
|
||||
f"Provided {key} '{value_name}' was not found!"
|
||||
)
|
||||
|
||||
if len(seq_uuid_mapping) > 0:
|
||||
# only the lowest prefix-list uuid is linkable - all others are just extensions of the first one
|
||||
self.p[value_name] = seq_uuid_mapping[min(seq_uuid_mapping.keys())]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['prefix_list_in'] not in [None, ''] and \
|
||||
entry['prefix_list_in'] in self.existing_prefixes:
|
||||
entry['prefix_list_in'] = self.existing_prefixes[entry['prefix_list_in']]['name']
|
||||
|
||||
if entry['prefix_list_out'] not in [None, ''] and \
|
||||
entry['prefix_list_out'] in self.existing_prefixes:
|
||||
entry['prefix_list_out'] = self.existing_prefixes[entry['prefix_list_out']]['name']
|
||||
|
||||
if entry['route_map_in'] not in [None, ''] and \
|
||||
entry['route_map_in'] in self.existing_maps:
|
||||
entry['route_map_in'] = self.existing_maps[entry['route_map_in']]['name']
|
||||
|
||||
if entry['route_map_out'] not in [None, ''] and \
|
||||
entry['route_map_out'] in self.existing_maps:
|
||||
entry['route_map_out'] = self.existing_maps[entry['route_map_out']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class PeerGroup(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addPeergroup',
|
||||
'del': 'delPeergroup',
|
||||
'set': 'setPeergroup',
|
||||
'search': 'get',
|
||||
'toggle': 'togglePeergroup',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.peergroups.peergroup'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'as_mode', 'as_number', 'source_int', 'next_hop_self', 'send_default_route',
|
||||
'prefix_list_in', 'prefix_list_out', 'route_map_in', 'route_map_out', 'listen_ranges',
|
||||
]
|
||||
FIELDS_ALL = ['enabled', 'name']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'as_mode': 'remote_as_mode',
|
||||
'as_number': 'remoteas',
|
||||
'source_int': 'updatesource',
|
||||
'next_hop_self': 'nexthopself',
|
||||
'send_default_route': 'defaultoriginate',
|
||||
'prefix_list_in': 'linkedPrefixlistIn',
|
||||
'prefix_list_out': 'linkedPrefixlistOut',
|
||||
'route_map_in': 'linkedRoutemapIn',
|
||||
'route_map_out': 'linkedRoutemapOut',
|
||||
'listen_ranges': 'listenranges',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': [
|
||||
'next_hop_self', 'enabled', 'send_default_route',
|
||||
],
|
||||
'select': [
|
||||
'as_mode', 'source_int', 'prefix_list_in', 'prefix_list_out',
|
||||
'route_map_in', 'route_map_out',
|
||||
],
|
||||
'list': ['listen_ranges'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'as_number': {'min': 1, 'max': 4294967295},
|
||||
}
|
||||
EXIST_ATTR = 'peergroup'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'bgp.prefixlists.prefixlist',
|
||||
'existing_maps': 'bgp.routemaps.routemap',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.peergroup = {}
|
||||
self.existing_prefixes = None
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
links = {
|
||||
'prefix-list-in': {
|
||||
'found': False,
|
||||
'existing': self.existing_prefixes,
|
||||
'match_fields': {'name': 'prefix_list_in'}
|
||||
},
|
||||
'prefix-list-out': {
|
||||
'found': False,
|
||||
'existing': self.existing_prefixes,
|
||||
'match_fields': {'name': 'prefix_list_out'}
|
||||
},
|
||||
'route-map-in': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map_in'}
|
||||
},
|
||||
'route-map-out': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map_out'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, values in links.items():
|
||||
value_name = values['match_fields']['name']
|
||||
provided = not is_unset(self.p[value_name])
|
||||
seq_uuid_mapping = {}
|
||||
|
||||
if not provided:
|
||||
continue
|
||||
|
||||
if len(values['existing']) > 0:
|
||||
for uuid, entry in values['existing'].items():
|
||||
matching = []
|
||||
|
||||
for api_field, ans_field in values['match_fields'].items():
|
||||
if not is_unset(self.p[ans_field]):
|
||||
matching.append(str(entry[api_field]) == str(self.p[ans_field]))
|
||||
|
||||
if all(matching):
|
||||
self.p[value_name] = uuid
|
||||
values['found'] = True
|
||||
|
||||
if 'seqnumber' in entry:
|
||||
seq_uuid_mapping[int(entry['seqnumber'])] = uuid
|
||||
|
||||
if not values['found']:
|
||||
self.m.fail_json(
|
||||
f"Provided {key} '{value_name}' was not found!"
|
||||
)
|
||||
|
||||
if len(seq_uuid_mapping) > 0:
|
||||
# only the lowest prefix-list uuid is linkable - all others are just extensions of the first one
|
||||
self.p[value_name] = seq_uuid_mapping[min(seq_uuid_mapping.keys())]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['prefix_list_in'] not in [None, ''] and \
|
||||
entry['prefix_list_in'] in self.existing_prefixes:
|
||||
entry['prefix_list_in'] = self.existing_prefixes[entry['prefix_list_in']]['name']
|
||||
|
||||
if entry['prefix_list_out'] not in [None, ''] and \
|
||||
entry['prefix_list_out'] in self.existing_prefixes:
|
||||
entry['prefix_list_out'] = self.existing_prefixes[entry['prefix_list_out']]['name']
|
||||
|
||||
if entry['route_map_in'] not in [None, ''] and \
|
||||
entry['route_map_in'] in self.existing_maps:
|
||||
entry['route_map_in'] = self.existing_maps[entry['route_map_in']]['name']
|
||||
|
||||
if entry['route_map_out'] not in [None, ''] and \
|
||||
entry['route_map_out'] in self.existing_maps:
|
||||
entry['route_map_out'] = self.existing_maps[entry['route_map_out']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Prefix(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addPrefixlist',
|
||||
'del': 'delPrefixlist',
|
||||
'set': 'setPrefixlist',
|
||||
'search': 'get',
|
||||
'toggle': 'togglePrefixlist',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.prefixlists.prefixlist'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['network', 'description', 'version', 'action']
|
||||
FIELDS_MATCH = ['seq', 'name']
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_MATCH)
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'seq': 'seqnumber',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['version', 'action'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'seq': {'min': 1, 'max': 4294967294},
|
||||
}
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[a-zA-Z0-9._-]{1,64}$'
|
||||
}
|
||||
EXIST_ATTR = 'prefix_list'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.prefix_list = {}
|
||||
self.existing_prefixes = None
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['network']) or is_unset(self.p['seq']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a BGP prefix-list you need to provide a network, '
|
||||
'sequence-number and action!'
|
||||
)
|
||||
|
||||
self._base_check(match_fields=self.FIELDS_MATCH)
|
||||
|
||||
def process(self) -> None:
|
||||
self.b.process()
|
||||
|
||||
def get_existing(self) -> list:
|
||||
return self.b.get_existing()
|
||||
|
||||
def create(self) -> None:
|
||||
self.b.create()
|
||||
|
||||
def update(self) -> None:
|
||||
self.b.update()
|
||||
|
||||
def delete(self) -> None:
|
||||
self.b.delete()
|
||||
|
||||
def reload(self) -> None:
|
||||
self.b.reload()
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Redistribution(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'addRedistribution',
|
||||
'del': 'delRedistribution',
|
||||
'set': 'setRedistribution',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRedistribution',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.redistributions.redistribution'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['redistribute', 'route_map']
|
||||
FIELDS_ALL = ['enabled', 'description']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'route_map': 'linkedRoutemap',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'select': ['redistribute', 'route_map',],
|
||||
}
|
||||
EXIST_ATTR = 'redistribute'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_maps': 'bgp.routemaps.routemap',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.redistribute = {}
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
links = {
|
||||
'route-map': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, values in links.items():
|
||||
value_name = values['match_fields']['name']
|
||||
provided = not is_unset(self.p[value_name])
|
||||
seq_uuid_mapping = {}
|
||||
|
||||
if not provided:
|
||||
continue
|
||||
|
||||
if len(values['existing']) > 0:
|
||||
for uuid, entry in values['existing'].items():
|
||||
matching = []
|
||||
|
||||
for api_field, ans_field in values['match_fields'].items():
|
||||
if not is_unset(self.p[ans_field]):
|
||||
matching.append(str(entry[api_field]) == str(self.p[ans_field]))
|
||||
|
||||
if all(matching):
|
||||
self.p[value_name] = uuid
|
||||
values['found'] = True
|
||||
|
||||
if 'seqnumber' in entry:
|
||||
seq_uuid_mapping[int(entry['seqnumber'])] = uuid
|
||||
|
||||
if not values['found']:
|
||||
self.m.fail_json(
|
||||
f"Provided {key} '{value_name}' was not found!"
|
||||
)
|
||||
|
||||
if len(seq_uuid_mapping) > 0:
|
||||
# only the lowest prefix-list uuid is linkable - all others are just extensions of the first one
|
||||
self.p[value_name] = seq_uuid_mapping[min(seq_uuid_mapping.keys())]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['route_map'] not in [None, ''] and \
|
||||
entry['route_map'] in self.existing_maps:
|
||||
entry['route_map'] = self.existing_maps[entry['route_map']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class RouteMap(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addRoutemap',
|
||||
'del': 'delRoutemap',
|
||||
'set': 'setRoutemap',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRoutemap',
|
||||
}
|
||||
API_KEY_PATH = 'bgp.routemaps.routemap'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'bgp'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'action', 'description', 'id', 'as_path_list', 'prefix_list',
|
||||
'community_list', 'set',
|
||||
]
|
||||
FIELDS_ALL = ['name', 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'as_path_list': 'match',
|
||||
'prefix_list': 'match2',
|
||||
'community_list': 'match3',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'int': ['id'],
|
||||
'list': ['as_path_list', 'prefix_list', 'community_list'],
|
||||
'select': ['action'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'id': {'min': 10, 'max': 99},
|
||||
}
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[a-zA-Z0-9._-]{1,64}$'
|
||||
}
|
||||
EXIST_ATTR = 'route_map'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_paths': 'bgp.aspaths.aspath',
|
||||
'existing_prefixes': 'bgp.prefixlists.prefixlist',
|
||||
'existing_communities': 'bgp.communitylists.communitylist',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.route_map = {}
|
||||
self.existing_paths = None
|
||||
self.existing_prefixes = None
|
||||
self.existing_communities = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['id']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a BGP route-map you need to provide an ID and action!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
self.b.find_multiple_links(
|
||||
field='as_path_list',
|
||||
existing=self.existing_paths,
|
||||
existing_field_id='description',
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='community_list',
|
||||
existing=self.existing_communities,
|
||||
existing_field_id='description',
|
||||
)
|
||||
|
||||
key = 'prefix_list'
|
||||
if len(self.p[key]) > 0:
|
||||
uuids = []
|
||||
provided_count = 0
|
||||
provided_prefixes = {}
|
||||
|
||||
for k, v in self.p[key].items():
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
|
||||
provided_count += len(v)
|
||||
provided_prefixes[k] = [int(_v) for _v in v]
|
||||
|
||||
if len(self.existing_prefixes) > 0:
|
||||
for uuid, entry in self.existing_prefixes.items():
|
||||
if entry['name'] in provided_prefixes and \
|
||||
int(entry['seqnumber']) in provided_prefixes[entry['name']]:
|
||||
uuids.append(uuid)
|
||||
|
||||
if len(uuids) != provided_count:
|
||||
self.m.fail_json(
|
||||
'At least one of the provided prefix-list entries was not found!'
|
||||
)
|
||||
|
||||
self.p[key] = uuids
|
||||
|
||||
else:
|
||||
self.p[key] = []
|
||||
self.r['diff']['after'][key] = self.p[key]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if len(entry['as_path_list']) > 0:
|
||||
_list = []
|
||||
for path in entry['as_path_list']:
|
||||
if path in self.existing_paths:
|
||||
_list.append(
|
||||
self.existing_paths[path]['description']
|
||||
)
|
||||
|
||||
entry['as_path_list'] = _list
|
||||
|
||||
if len(entry['prefix_list']) > 0:
|
||||
_list = []
|
||||
for pre in entry['prefix_list']:
|
||||
if pre in self.existing_prefixes:
|
||||
_list.append(
|
||||
self.existing_prefixes[pre]['name']
|
||||
)
|
||||
|
||||
entry['prefix_list'] = _list
|
||||
|
||||
if len(entry['community_list']) > 0:
|
||||
_list = []
|
||||
for comm in entry['community_list']:
|
||||
if comm in self.existing_communities:
|
||||
_list.append(
|
||||
self.existing_communities[comm]['description']
|
||||
)
|
||||
|
||||
entry['community_list'] = _list
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'general'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'general'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'enabled', 'profile', 'carp', 'log', 'snmp_agentx', 'log_level',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'carp': 'enablecarp',
|
||||
'log': 'enablesyslog',
|
||||
'snmp_agentx': 'enablesnmp',
|
||||
'log_level': 'sysloglevel',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'carp', 'log', 'snmp_agentx'],
|
||||
'select': ['log_level', 'profile'],
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'carp', 'id', 'enabled',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'carp': 'carp_demote',
|
||||
'id': 'routerid',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'carp'],
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Interface(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addInterface',
|
||||
'del': 'delInterface',
|
||||
'set': 'setInterface',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleInterface',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6.interfaces.interface'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'interface', 'area', 'passive', 'cost', 'cost_demoted', 'carp_depend_on',
|
||||
'hello_interval', 'dead_interval', 'retransmit_interval', 'transmit_delay',
|
||||
'priority', 'network_type',
|
||||
]
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
INT_VALIDATIONS = {
|
||||
'cost': {'min': 0, 'max': 4294967295},
|
||||
'hello_interval': {'min': 0, 'max': 4294967295},
|
||||
'dead_interval': {'min': 0, 'max': 4294967295},
|
||||
'retransmit_interval': {'min': 0, 'max': 4294967295},
|
||||
'transmit_delay': {'min': 0, 'max': 4294967295},
|
||||
'priority': {'min': 0, 'max': 4294967295},
|
||||
'cost_demoted': {'min': 1, 'max': 65535},
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'interface': 'interfacename',
|
||||
'hello_interval': 'hellointerval',
|
||||
'dead_interval': 'deadinterval',
|
||||
'retransmit_interval': 'retransmitinterval',
|
||||
'transmit_delay': 'transmitdelay',
|
||||
'network_type': 'networktype',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'passive'],
|
||||
'select': ['interface', 'carp_depend_on', 'network_type'],
|
||||
}
|
||||
EXIST_ATTR = 'int'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.int = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['area']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPFv3 interface you need to provide its area!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip_or_network, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Network(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addNetwork',
|
||||
'del': 'delNetwork',
|
||||
'set': 'setNetwork',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleNetwork',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6.networks.network'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['ip', 'mask', 'area', 'area_range', 'prefix_list_in', 'prefix_list_out']
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'ip': 'ipaddr',
|
||||
'mask': 'netmask',
|
||||
'area_range': 'arearange',
|
||||
'prefix_list_in': 'linkedPrefixlistIn',
|
||||
'prefix_list_out': 'linkedPrefixlistOut',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['prefix_list_in', 'prefix_list_out'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'mask': {'min': 0, 'max': 128},
|
||||
}
|
||||
EXIST_ATTR = 'net'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'ospf6.prefixlists.prefixlist',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.net = {}
|
||||
self.existing_prefixes = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['area']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF network you need to provide an area!'
|
||||
)
|
||||
|
||||
if not is_ip_or_network(f"{self.p['ip']}/{self.p['mask']}", strict=True):
|
||||
self.m.fail_json(
|
||||
'The combination of the provided ip and network mask is invalid: '
|
||||
f"'{self.p['ip']}/{self.p['mask']}'!"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='prefix_list_in',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
self.b.find_single_link(
|
||||
field='prefix_list_out',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['prefix_list_in'] not in [None, ''] and \
|
||||
entry['prefix_list_in'] in self.existing_prefixes:
|
||||
entry['prefix_list_in'] = self.existing_prefixes[entry['prefix_list_in']]['name']
|
||||
|
||||
if entry['prefix_list_out'] not in [None, ''] and \
|
||||
entry['prefix_list_out'] in self.existing_prefixes:
|
||||
entry['prefix_list_out'] = self.existing_prefixes[entry['prefix_list_out']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Prefix(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addPrefixlist',
|
||||
'del': 'delPrefixlist',
|
||||
'set': 'setPrefixlist',
|
||||
'search': 'get',
|
||||
'toggle': 'togglePrefixlist',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6.prefixlists.prefixlist'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['seq', 'action', 'network']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
INT_VALIDATIONS = {
|
||||
'seq': {'min': 10, 'max': 99},
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'seq': 'seqnumber',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
}
|
||||
EXIST_ATTR = 'prefix'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.prefix = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['seq']) or is_unset(self.p['action']) or is_unset(self.p['network']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF prefix-list you need to provide its sequence-number, '
|
||||
'action and network!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Redistribution(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'addRedistribution',
|
||||
'del': 'delRedistribution',
|
||||
'set': 'setRedistribution',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRedistribution',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6.redistributions.redistribution'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['redistribute', 'route_map']
|
||||
FIELDS_ALL = ['enabled', 'description']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'route_map': 'linkedRoutemap',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'select': ['redistribute', 'route_map',],
|
||||
}
|
||||
EXIST_ATTR = 'redistribute'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_maps': 'ospf6.routemaps.routemap',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.redistribute = {}
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
links = {
|
||||
'route-map': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, values in links.items():
|
||||
value_name = values['match_fields']['name']
|
||||
provided = not is_unset(self.p[value_name])
|
||||
seq_uuid_mapping = {}
|
||||
|
||||
if not provided:
|
||||
continue
|
||||
|
||||
if len(values['existing']) > 0:
|
||||
for uuid, entry in values['existing'].items():
|
||||
matching = []
|
||||
|
||||
for api_field, ans_field in values['match_fields'].items():
|
||||
if not is_unset(self.p[ans_field]):
|
||||
matching.append(str(entry[api_field]) == str(self.p[ans_field]))
|
||||
|
||||
if all(matching):
|
||||
self.p[value_name] = uuid
|
||||
values['found'] = True
|
||||
|
||||
if 'seqnumber' in entry:
|
||||
seq_uuid_mapping[int(entry['seqnumber'])] = uuid
|
||||
|
||||
if not values['found']:
|
||||
self.m.fail_json(
|
||||
f"Provided {key} '{value_name}' was not found!"
|
||||
)
|
||||
|
||||
if len(seq_uuid_mapping) > 0:
|
||||
# only the lowest prefix-list uuid is linkable - all others are just extensions of the first one
|
||||
self.p[value_name] = seq_uuid_mapping[min(seq_uuid_mapping.keys())]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['route_map'] not in [None, ''] and \
|
||||
entry['route_map'] in self.existing_maps:
|
||||
entry['route_map'] = self.existing_maps[entry['route_map']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class RouteMap(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addRoutemap',
|
||||
'del': 'delRoutemap',
|
||||
'set': 'setRoutemap',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRoutemap',
|
||||
}
|
||||
API_KEY_PATH = 'ospf6.routemaps.routemap'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospf6settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['action', 'id', 'prefix_list', 'set']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'prefix_list': 'match2',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'list': ['prefix_list'],
|
||||
'select': ['action'],
|
||||
'int': ['id'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'id': {'min': 10, 'max': 99},
|
||||
}
|
||||
EXIST_ATTR = 'route_map'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'ospf6.prefixlists.prefixlist',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.route_map = {}
|
||||
self.existing_prefixes = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['id']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF route-map you need to provide an ID and action!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_multiple_links(
|
||||
field='prefix_list',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if len(entry['prefix_list']) > 0:
|
||||
_list = []
|
||||
for pre in entry['prefix_list']:
|
||||
if pre in self.existing_prefixes:
|
||||
_list.append(
|
||||
self.existing_prefixes[pre]['name']
|
||||
)
|
||||
|
||||
entry['prefix_list'] = _list
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'ospf'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'carp', 'id', 'cost', 'enabled', 'passive_ints',
|
||||
'originate', 'originate_always', 'originate_metric',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'carp': 'carp_demote',
|
||||
'id': 'routerid',
|
||||
'cost': 'costreference',
|
||||
'originate_always': 'originatealways',
|
||||
'originate_metric': 'originatemetric',
|
||||
'passive_ints': 'passiveinterfaces',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'carp', 'originate', 'originate_always'],
|
||||
'list': ['passive_ints'],
|
||||
'int': ['originate_metric', 'cost'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'cost': {'min': 1, 'max': 4294967},
|
||||
'originate_metric': {'min': 0, 'max': 16777214},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Interface(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addInterface',
|
||||
'del': 'delInterface',
|
||||
'set': 'setInterface',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleInterface',
|
||||
}
|
||||
API_KEY_PATH = 'ospf.interfaces.interface'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'interface', 'area', 'auth_type', 'auth_key', 'auth_key_id', 'cost',
|
||||
'hello_interval', 'dead_interval', 'retransmit_interval', 'transmit_delay',
|
||||
'priority', 'network_type', 'carp_depend_on', 'cost_demoted',
|
||||
]
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
INT_VALIDATIONS = {
|
||||
'cost': {'min': 1, 'max': 65535},
|
||||
'hello_interval': {'min': 0, 'max': 4294967295},
|
||||
'dead_interval': {'min': 0, 'max': 4294967295},
|
||||
'retransmit_interval': {'min': 0, 'max': 4294967295},
|
||||
'transmit_delay': {'min': 0, 'max': 4294967295},
|
||||
'priority': {'min': 0, 'max': 4294967295},
|
||||
'cost_demoted': {'min': 1, 'max': 65535},
|
||||
'auth_key_id': {'min': 1, 'max': 255},
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'interface': 'interfacename',
|
||||
'hello_interval': 'hellointerval',
|
||||
'dead_interval': 'deadinterval',
|
||||
'retransmit_interval': 'retransmitinterval',
|
||||
'transmit_delay': 'transmitdelay',
|
||||
'network_type': 'networktype',
|
||||
'auth_type': 'authtype',
|
||||
'auth_key': 'authkey',
|
||||
'auth_key_id': 'authkey_id',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['interface', 'carp_depend_on', 'network_type', 'auth_type'],
|
||||
}
|
||||
EXIST_ATTR = 'int'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.int = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['area']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF interface you need to provide its area!'
|
||||
)
|
||||
|
||||
if not is_unset(self.p['auth_type']) and is_unset(self.p['auth_key']):
|
||||
self.m.fail_json(
|
||||
'You need to provide an authentication-key if you enable authentication!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip_or_network, is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Network(BaseModule):
|
||||
CMDS = {
|
||||
'add': 'addNetwork',
|
||||
'del': 'delNetwork',
|
||||
'set': 'setNetwork',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleNetwork',
|
||||
}
|
||||
API_KEY_PATH = 'ospf.networks.network'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['ip', 'mask', 'area', 'area_range', 'prefix_list_in', 'prefix_list_out']
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'ip': 'ipaddr',
|
||||
'mask': 'netmask',
|
||||
'area_range': 'arearange',
|
||||
'prefix_list_in': 'linkedPrefixlistIn',
|
||||
'prefix_list_out': 'linkedPrefixlistOut',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['prefix_list_in', 'prefix_list_out'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'mask': {'min': 0, 'max': 32},
|
||||
}
|
||||
EXIST_ATTR = 'net'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'ospf.prefixlists.prefixlist',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.net = {}
|
||||
self.existing_prefixes = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['area']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF network you need to provide an area!'
|
||||
)
|
||||
|
||||
if not is_ip_or_network(f"{self.p['ip']}/{self.p['mask']}", strict=True):
|
||||
self.m.fail_json(
|
||||
'The combination of the provided ip and network mask is invalid: '
|
||||
f"'{self.p['ip']}/{self.p['mask']}'!"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_single_link(
|
||||
field='prefix_list_in',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
self.b.find_single_link(
|
||||
field='prefix_list_out',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['prefix_list_in'] not in [None, ''] and \
|
||||
entry['prefix_list_in'] in self.existing_prefixes:
|
||||
entry['prefix_list_in'] = self.existing_prefixes[entry['prefix_list_in']]['name']
|
||||
|
||||
if entry['prefix_list_out'] not in [None, ''] and \
|
||||
entry['prefix_list_out'] in self.existing_prefixes:
|
||||
entry['prefix_list_out'] = self.existing_prefixes[entry['prefix_list_out']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Prefix(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addPrefixlist',
|
||||
'del': 'delPrefixlist',
|
||||
'set': 'setPrefixlist',
|
||||
'search': 'get',
|
||||
'toggle': 'togglePrefixlist',
|
||||
}
|
||||
API_KEY_PATH = 'ospf.prefixlists.prefixlist'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['seq', 'action', 'network']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
INT_VALIDATIONS = {
|
||||
'seq': {'min': 10, 'max': 99},
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'seq': 'seqnumber',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
}
|
||||
EXIST_ATTR = 'prefix'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.prefix = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['seq']) or is_unset(self.p['action']) or is_unset(self.p['network']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF prefix-list you need to provide its sequence-number, '
|
||||
'action and network!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Redistribution(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'addRedistribution',
|
||||
'del': 'delRedistribution',
|
||||
'set': 'setRedistribution',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRedistribution',
|
||||
}
|
||||
API_KEY_PATH = 'ospf.redistributions.redistribution'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['redistribute', 'route_map']
|
||||
FIELDS_ALL = ['enabled', 'description']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'route_map': 'linkedRoutemap',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'select': ['redistribute', 'route_map',],
|
||||
}
|
||||
EXIST_ATTR = 'redistribute'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_maps': 'ospf.routemaps.routemap',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.redistribute = {}
|
||||
self.existing_maps = None
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
self._find_links()
|
||||
|
||||
def _find_links(self) -> None:
|
||||
links = {
|
||||
'route-map': {
|
||||
'found': False,
|
||||
'existing': self.existing_maps,
|
||||
'match_fields': {'name': 'route_map'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, values in links.items():
|
||||
value_name = values['match_fields']['name']
|
||||
provided = not is_unset(self.p[value_name])
|
||||
seq_uuid_mapping = {}
|
||||
|
||||
if not provided:
|
||||
continue
|
||||
|
||||
if len(values['existing']) > 0:
|
||||
for uuid, entry in values['existing'].items():
|
||||
matching = []
|
||||
|
||||
for api_field, ans_field in values['match_fields'].items():
|
||||
if not is_unset(self.p[ans_field]):
|
||||
matching.append(str(entry[api_field]) == str(self.p[ans_field]))
|
||||
|
||||
if all(matching):
|
||||
self.p[value_name] = uuid
|
||||
values['found'] = True
|
||||
|
||||
if 'seqnumber' in entry:
|
||||
seq_uuid_mapping[int(entry['seqnumber'])] = uuid
|
||||
|
||||
if not values['found']:
|
||||
self.m.fail_json(
|
||||
f"Provided {key} '{value_name}' was not found!"
|
||||
)
|
||||
|
||||
if len(seq_uuid_mapping) > 0:
|
||||
# only the lowest prefix-list uuid is linkable - all others are just extensions of the first one
|
||||
self.p[value_name] = seq_uuid_mapping[min(seq_uuid_mapping.keys())]
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if entry['route_map'] not in [None, ''] and \
|
||||
entry['route_map'] in self.existing_maps:
|
||||
entry['route_map'] = self.existing_maps[entry['route_map']]['name']
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class RouteMap(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'addRoutemap',
|
||||
'del': 'delRoutemap',
|
||||
'set': 'setRoutemap',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleRoutemap',
|
||||
}
|
||||
API_KEY_PATH = 'ospf.routemaps.routemap'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'ospfsettings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['action', 'id', 'prefix_list', 'set']
|
||||
FIELDS_ALL = [FIELD_ID, 'enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'prefix_list': 'match2',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'list': ['prefix_list'],
|
||||
'select': ['action'],
|
||||
'int': ['id'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'id': {'min': 10, 'max': 99},
|
||||
}
|
||||
EXIST_ATTR = 'route_map'
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_prefixes': 'ospf.prefixlists.prefixlist',
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.route_map = {}
|
||||
self.existing_prefixes = None
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['id']) or is_unset(self.p['action']):
|
||||
self.m.fail_json(
|
||||
'To create a OSPF route-map you need to provide an ID and action!'
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_multiple_links(
|
||||
field='prefix_list',
|
||||
existing=self.existing_prefixes,
|
||||
)
|
||||
|
||||
def get_existing(self) -> list:
|
||||
existing = []
|
||||
|
||||
for entry in self.b.get_existing():
|
||||
if len(entry['prefix_list']) > 0:
|
||||
_list = []
|
||||
for pre in entry['prefix_list']:
|
||||
if pre in self.existing_prefixes:
|
||||
_list.append(
|
||||
self.existing_prefixes[pre]['name']
|
||||
)
|
||||
|
||||
entry['prefix_list'] = _list
|
||||
|
||||
existing.append(entry)
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class Rip(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'rip'
|
||||
API_MOD = 'quagga'
|
||||
API_CONT = 'rip'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'version', 'metric', 'passive_ints', 'enabled', 'networks',
|
||||
'redistribute',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'passive_ints': 'passiveinterfaces',
|
||||
'metric': 'defaultmetric',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'list': ['passive_ints', 'networks', 'redistribute'],
|
||||
'int': ['version'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'version': {'min': 1, 'max': 2},
|
||||
'metric': {'min': 1, 'max': 16},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
from ipaddress import ip_address
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip4, is_ip6
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import is_unset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Gw(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add_gateway',
|
||||
'del': 'del_gateway',
|
||||
'set': 'set_gateway',
|
||||
'search': 'search_gateway',
|
||||
'detail': 'get_gateway',
|
||||
'toggle': 'toggle_gateway',
|
||||
}
|
||||
API_KEY_PATH = 'gateway_item'
|
||||
API_MOD = 'routing'
|
||||
API_CONT = 'settings'
|
||||
FIELDS_CHANGE = [
|
||||
'name', 'interface', 'gateway', 'default_gw', 'far_gw', 'monitor_disable', 'monitor_noroute', 'monitor',
|
||||
'force_down', 'priority', 'weight', 'latency_low', 'latency_high', 'loss_low', 'loss_high', 'interval',
|
||||
'time_period', 'loss_interval', 'data_length', 'description', 'ip_protocol',
|
||||
]
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_BOOL_INVERT = ['enabled']
|
||||
FIELDS_TRANSLATE = {
|
||||
'description': 'descr',
|
||||
'ip_protocol': 'ipprotocol',
|
||||
'enabled': 'disabled',
|
||||
'default_gw': 'defaultgw',
|
||||
'far_gw': 'fargw',
|
||||
'latency_low': 'latencylow',
|
||||
'latency_high': 'latencyhigh',
|
||||
'loss_low': 'losslow',
|
||||
'loss_high': 'losshigh',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'default_gw', 'far_gw', 'monitor_disable', 'monitor_noroute', 'force_down'],
|
||||
'int': [
|
||||
'priority', 'weight', 'latency_low', 'latency_high', 'loss_low', 'loss_high', 'interval', 'time_period',
|
||||
'loss_interval', 'data_length',
|
||||
],
|
||||
'select': ['interface', 'ip_protocol'],
|
||||
}
|
||||
INT_VALIDATIONS = {
|
||||
'priority': {'min': 0, 'max': 255},
|
||||
'weight': {'min': 1, 'max': 5},
|
||||
'latency_low': {'min': 1, 'max': 9999},
|
||||
'latency_high': {'min': 1, 'max': 9999},
|
||||
'loss_low': {'min': 1, 'max': 99},
|
||||
'loss_high': {'min': 1, 'max': 99},
|
||||
'interval': {'min': 1, 'max': 9999},
|
||||
'time_period': {'min': 1, 'max': 9999},
|
||||
'data_length': {'min': 0, 'max': 9999},
|
||||
}
|
||||
EXIST_ATTR = 'gw'
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.gw = {}
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['state'] == 'present':
|
||||
if not is_unset(self.p['gateway']):
|
||||
try:
|
||||
ip_address(self.p['gateway'])
|
||||
|
||||
except ValueError:
|
||||
self.m.fail_json(f"Value '{self.p['gateway']}' is not a valid gateway!")
|
||||
|
||||
if self.p['ip_protocol'] == 'inet' and not is_ip4(self.p['gateway']):
|
||||
self.m.fail_json(f"Gateway '{self.p['gateway']}' is not a valid IPv4-address!")
|
||||
elif self.p['ip_protocol'] == 'inet6' and not is_ip6(self.p['gateway']):
|
||||
self.m.fail_json(f"Gateway '{self.p['gateway']}' is not a valid IPv6-address!")
|
||||
|
||||
if self.p['monitor']:
|
||||
try:
|
||||
ip_address(self.p['monitor'])
|
||||
|
||||
except ValueError:
|
||||
self.m.fail_json(f"Value '{self.p['monitor']}' is not a valid monitor address!")
|
||||
|
||||
if not self.p['interface']:
|
||||
self.m.fail_json('You need to provide a value for the interface!')
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_unset, get_key_by_value_from_selection
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class Group(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
CMDS = {
|
||||
'add': 'add',
|
||||
'del': 'del',
|
||||
'set': 'set',
|
||||
'search': 'search',
|
||||
'detail': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'group'
|
||||
API_MOD = 'auth'
|
||||
API_CONT = 'group'
|
||||
FIELDS_CHANGE = ['description', 'source_net', 'privilege', 'member']
|
||||
FIELDS_TYPING = {
|
||||
'list': ['privilege', 'source_net'],
|
||||
'list_value': ['member'],
|
||||
}
|
||||
FIELDS_TRANSLATE = {
|
||||
'privilege': 'priv',
|
||||
'source_net': 'source_networks',
|
||||
}
|
||||
FIELDS_ALL = ['name']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
EXIST_ATTR = 'group'
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[a-zA-Z0-9._-]{1,32}$'
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.group = {}
|
||||
|
||||
def delete(self) -> None:
|
||||
if self.group['scope'] == 'system':
|
||||
self.m.fail_json(f"Not allowed to delete system group {self.group['name']}")
|
||||
|
||||
self.b.delete()
|
||||
|
||||
def _build_request(self) -> dict:
|
||||
raw_request = self.b.build_request()
|
||||
|
||||
if not is_unset(self.p['member']):
|
||||
# translate user-names to user-id's
|
||||
raw_request[self.API_KEY_PATH]['member'] = self.b.RESP_JOIN_CHAR.join([
|
||||
get_key_by_value_from_selection(self.b.raw['member'], m)
|
||||
for m in self.p['member']
|
||||
])
|
||||
|
||||
return raw_request
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import is_unset
|
||||
|
||||
|
||||
class HaproxyAcl(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addAcl',
|
||||
'del': 'delAcl',
|
||||
'set': 'setAcl',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleAcl',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.acls.acl'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'case_sensitive': 'caseSensitive',
|
||||
'allowed_users': 'allowedUsers',
|
||||
'allowed_groups': 'allowedGroups',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + [
|
||||
'name', 'description', 'expression', 'negate',
|
||||
'hdr_beg', 'hdr_end', 'hdr', 'hdr_reg', 'hdr_sub',
|
||||
'path_beg', 'path_end', 'path', 'path_reg', 'path_dir', 'path_sub',
|
||||
'cust_hdr_beg_name', 'cust_hdr_beg', 'cust_hdr_end_name', 'cust_hdr_end',
|
||||
'cust_hdr_name', 'cust_hdr', 'cust_hdr_reg_name', 'cust_hdr_reg',
|
||||
'cust_hdr_sub_name', 'cust_hdr_sub', 'url_param', 'url_param_value',
|
||||
'ssl_c_verify_code', 'ssl_c_ca_commonname', 'ssl_hello_type',
|
||||
'src', 'src_port', 'src_port_comparison', 'nbsrv', 'nbsrv_backend',
|
||||
'ssl_fc_sni', 'ssl_sni', 'ssl_sni_sub', 'ssl_sni_beg',
|
||||
'ssl_sni_end', 'ssl_sni_reg', 'custom_acl',
|
||||
'src_bytes_in_rate_comparison', 'src_bytes_in_rate',
|
||||
'src_bytes_out_rate_comparison', 'src_bytes_out_rate',
|
||||
'src_conn_cnt_comparison', 'src_conn_cnt',
|
||||
'src_conn_cur_comparison', 'src_conn_cur',
|
||||
'src_conn_rate_comparison', 'src_conn_rate',
|
||||
'src_http_err_cnt_comparison', 'src_http_err_cnt',
|
||||
'src_http_err_rate_comparison', 'src_http_err_rate',
|
||||
'src_http_req_cnt_comparison', 'src_http_req_cnt',
|
||||
'src_http_req_rate_comparison', 'src_http_req_rate',
|
||||
'src_kbytes_in_comparison', 'src_kbytes_in',
|
||||
'src_kbytes_out_comparison', 'src_kbytes_out',
|
||||
'src_sess_cnt_comparison', 'src_sess_cnt',
|
||||
'src_sess_rate_comparison', 'src_sess_rate'
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'str': ['name', 'description', 'hdr_beg', 'hdr_end', 'hdr', 'hdr_reg', 'hdr_sub',
|
||||
'path_beg', 'path_end', 'path', 'path_reg', 'path_dir', 'path_sub',
|
||||
'cust_hdr_beg_name', 'cust_hdr_beg', 'cust_hdr_end_name', 'cust_hdr_end',
|
||||
'cust_hdr_name', 'cust_hdr', 'cust_hdr_reg_name', 'cust_hdr_reg',
|
||||
'cust_hdr_sub_name', 'cust_hdr_sub', 'url_param', 'url_param_value',
|
||||
'ssl_c_ca_commonname', 'src', 'ssl_fc_sni', 'ssl_sni', 'ssl_sni_sub',
|
||||
'ssl_sni_beg', 'ssl_sni_end', 'ssl_sni_reg', 'custom_acl'],
|
||||
'bool': ['negate', 'case_sensitive'],
|
||||
'int': ['ssl_c_verify_code', 'src_port', 'nbsrv', 'src_bytes_in_rate', 'src_bytes_out_rate',
|
||||
'src_conn_cnt', 'src_conn_cur', 'src_conn_rate', 'src_http_err_cnt', 'src_http_err_rate',
|
||||
'src_http_req_cnt', 'src_http_req_rate', 'src_kbytes_in', 'src_kbytes_out',
|
||||
'src_sess_cnt', 'src_sess_rate'],
|
||||
'list': ['allowed_users', 'allowed_groups'],
|
||||
'select': ['expression', 'ssl_hello_type', 'nbsrv_backend', 'src_port_comparison',
|
||||
'src_bytes_in_rate_comparison', 'src_bytes_out_rate_comparison', 'src_conn_cnt_comparison',
|
||||
'src_conn_cur_comparison', 'src_conn_rate_comparison', 'src_http_err_cnt_comparison',
|
||||
'src_http_err_rate_comparison', 'src_http_req_cnt_comparison', 'src_http_req_rate_comparison',
|
||||
'src_kbytes_in_comparison', 'src_kbytes_out_comparison', 'src_sess_cnt_comparison',
|
||||
'src_sess_rate_comparison']
|
||||
}
|
||||
|
||||
EXIST_ATTR = 'haproxy_acl'
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$',
|
||||
}
|
||||
|
||||
### TODO : Uncomment backends when implemented
|
||||
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_users': 'haproxy.users.user',
|
||||
'existing_groups': 'haproxy.groups.group',
|
||||
# 'existing_backends': 'haproxy.backends.backend',
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_acl = {}
|
||||
self.existing_users = {}
|
||||
self.existing_groups = {}
|
||||
# self.existing_backends = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
if is_unset(self.p['expression']):
|
||||
self.m.fail_json("You need to provide an 'expression' to create an ACL!")
|
||||
|
||||
if self.p.get('allowed_users'):
|
||||
self.b.find_multiple_links(
|
||||
field='allowed_users',
|
||||
existing=self.existing_users,
|
||||
existing_field_id='name',
|
||||
)
|
||||
if self.p.get('allowed_groups'):
|
||||
self.b.find_multiple_links(
|
||||
field='allowed_groups',
|
||||
existing=self.existing_groups,
|
||||
existing_field_id='name',
|
||||
)
|
||||
# if self.p.get('nbsrv_backend'):
|
||||
# self.b.find_single_link(
|
||||
# field='nbsrv_backend',
|
||||
# existing=self.existing_backends,
|
||||
# existing_field_id='name',
|
||||
# )
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyAction(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addAction',
|
||||
'del': 'delAction',
|
||||
'set': 'setAction',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleAction',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.actions.action'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'test_type': 'testType',
|
||||
'linked_acls': 'linkedAcls',
|
||||
'custom_rule': 'custom',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + [
|
||||
'name', 'description', 'operator', 'type',
|
||||
'use_backend', 'use_server', 'fcgi_pass_header', 'fcgi_set_param',
|
||||
'http_request_auth', 'http_request_redirect', 'http_request_lua',
|
||||
'http_request_use_service', 'http_request_add_header_name',
|
||||
'http_request_add_header_content', 'http_request_set_header_name',
|
||||
'http_request_set_header_content', 'http_request_del_header_name',
|
||||
'http_request_replace_header_name', 'http_request_replace_header_regex',
|
||||
'http_request_replace_value_name', 'http_request_replace_value_regex',
|
||||
'http_request_set_path', 'http_request_set_var_scope',
|
||||
'http_request_set_var_name', 'http_request_set_var_expr',
|
||||
'http_response_lua', 'http_response_add_header_name',
|
||||
'http_response_add_header_content', 'http_response_set_header_name',
|
||||
'http_response_set_header_content', 'http_response_del_header_name',
|
||||
'http_response_replace_header_name', 'http_response_replace_header_regex',
|
||||
'http_response_replace_value_name', 'http_response_replace_value_regex',
|
||||
'http_response_set_status_code', 'http_response_set_status_reason',
|
||||
'http_response_set_var_scope', 'http_response_set_var_name',
|
||||
'http_response_set_var_expr', 'monitor_fail_uri',
|
||||
'tcp_request_content_lua', 'tcp_request_content_use_service',
|
||||
'tcp_request_inspect_delay', 'tcp_response_content_lua',
|
||||
'tcp_response_inspect_delay',
|
||||
'map_use_backend_file', 'map_use_backend_default'
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
"str": [
|
||||
"name",
|
||||
"description",
|
||||
"fcgi_pass_header",
|
||||
"fcgi_set_param",
|
||||
"http_request_auth",
|
||||
"http_request_redirect",
|
||||
"http_request_use_service",
|
||||
"http_request_add_header_name",
|
||||
"http_request_add_header_content",
|
||||
"http_request_set_header_name",
|
||||
"http_request_set_header_content",
|
||||
"http_request_del_header_name",
|
||||
"http_request_replace_header_name",
|
||||
"http_request_replace_header_regex",
|
||||
"http_request_replace_value_name",
|
||||
"http_request_replace_value_regex",
|
||||
"http_request_set_path",
|
||||
"http_request_set_var_name",
|
||||
"http_request_set_var_expr",
|
||||
"http_response_add_header_name",
|
||||
"http_response_add_header_content",
|
||||
"http_response_set_header_name",
|
||||
"http_response_set_header_content",
|
||||
"http_response_del_header_name",
|
||||
"http_response_replace_header_name",
|
||||
"http_response_replace_header_regex",
|
||||
"http_response_replace_value_name",
|
||||
"http_response_replace_value_regex",
|
||||
"http_response_set_status_reason",
|
||||
"http_response_set_var_name",
|
||||
"http_response_set_var_expr",
|
||||
"monitor_fail_uri",
|
||||
"tcp_request_content_use_service",
|
||||
"tcp_request_inspect_delay",
|
||||
"tcp_response_inspect_delay",
|
||||
"custom_rule",
|
||||
],
|
||||
"int": ["http_response_set_status_code"],
|
||||
"list": ["linked_acls"],
|
||||
"select": [
|
||||
"test_type",
|
||||
"operator",
|
||||
"type",
|
||||
"http_request_set_var_scope",
|
||||
"http_response_set_var_scope",
|
||||
"use_backend",
|
||||
"use_server",
|
||||
"http_request_lua",
|
||||
"http_response_lua",
|
||||
"tcp_request_content_lua",
|
||||
"tcp_response_content_lua",
|
||||
"map_use_backend_file",
|
||||
"map_use_backend_default",
|
||||
],
|
||||
}
|
||||
|
||||
EXIST_ATTR = 'haproxy_action'
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$',
|
||||
}
|
||||
|
||||
### TODO : Uncomment backends, servers, and mapping files when implemented
|
||||
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_acls': 'haproxy.acls.acl',
|
||||
# 'existing_backends': 'haproxy.backends.backend',
|
||||
# 'existing_servers': 'haproxy.servers.server',
|
||||
'existing_luas': 'haproxy.luas.lua',
|
||||
# 'existing_mapfiles': 'haproxy.mapfiles.mapfile',
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_action = {}
|
||||
self.existing_acls = {}
|
||||
# self.existing_backends = {}
|
||||
# self.existing_servers = {}
|
||||
self.existing_luas = {}
|
||||
# self.existing_mapfiles = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
if self.p['state'] == 'present':
|
||||
if self.p.get('linked_acls'):
|
||||
self.b.find_multiple_links(
|
||||
field='linked_acls',
|
||||
existing=self.existing_acls,
|
||||
)
|
||||
# if self.p.get('use_backend'):
|
||||
# self.b.find_single_link(
|
||||
# field='use_backend',
|
||||
# existing=self.existing_backends,
|
||||
# )
|
||||
# if self.p.get('use_server'):
|
||||
# self.b.find_single_link(
|
||||
# field='use_server',
|
||||
# existing=self.existing_servers,
|
||||
# )
|
||||
if self.p.get('http_request_lua'):
|
||||
self.b.find_single_link(
|
||||
field='http_request_lua',
|
||||
existing=self.existing_luas,
|
||||
)
|
||||
if self.p.get('http_response_lua'):
|
||||
self.b.find_single_link(
|
||||
field='http_response_lua',
|
||||
existing=self.existing_luas,
|
||||
)
|
||||
if self.p.get('tcp_request_content_lua'):
|
||||
self.b.find_single_link(
|
||||
field='tcp_request_content_lua',
|
||||
existing=self.existing_luas,
|
||||
)
|
||||
if self.p.get('tcp_response_content_lua'):
|
||||
self.b.find_single_link(
|
||||
field='tcp_response_content_lua',
|
||||
existing=self.existing_luas,
|
||||
)
|
||||
# if self.p.get('map_use_backend_file'):
|
||||
# self.b.find_single_link(
|
||||
# field='map_use_backend_file',
|
||||
# existing=self.existing_mapfiles,
|
||||
# )
|
||||
# if self.p.get('map_use_backend_default'):
|
||||
# self.b.find_single_link(
|
||||
# field='map_use_backend_default',
|
||||
# existing=self.existing_backends,
|
||||
# )
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyCpu(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addCpu',
|
||||
'del': 'delCpu',
|
||||
'set': 'setCpu',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleCpu',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.cpus.cpu'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name', 'thread_id', 'cpu_id']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
EXIST_ATTR = 'cpu'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['thread_id'],
|
||||
'list': ['cpu_id'],
|
||||
}
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[0-9a-zA-Z._-]{1,255}$', # Name validation from XML model
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.cpu = {}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyErrorfile(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addErrorfile',
|
||||
'del': 'delErrorfile',
|
||||
'set': 'setErrorfile',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleErrorfile',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.errorfiles.errorfile'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['name', 'description', 'code', 'content']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
EXIST_ATTR = 'haproxy_errorfile'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'select': ['code'],
|
||||
}
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$',
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_errorfile = {}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyFcgi(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addFcgi',
|
||||
'del': 'delFcgi',
|
||||
'set': 'setFcgi',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleFcgi',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.fcgis.fcgi'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'linked_actions': 'linkedActions'
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name', 'description', 'docroot', 'index', 'path_info',
|
||||
'log_stderr', 'keep_conn', 'get_values', 'mpxs_conns', 'max_reqs', 'linked_actions']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
EXIST_ATTR = 'haproxy_fcgi'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'log_stderr', 'keep_conn', 'get_values', 'mpxs_conns'],
|
||||
'int': ['max_reqs'],
|
||||
'list': ['linked_actions']
|
||||
}
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$',
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'max_reqs': {'min': 1, 'max': 100000},
|
||||
}
|
||||
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_actions': 'haproxy.actions.action',
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_fcgi = {}
|
||||
self.existing_actions = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_multiple_links(
|
||||
field='linked_actions',
|
||||
existing=self.existing_actions,
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralCache(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.cache'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'total_max_size': 'totalMaxSize',
|
||||
'max_age': 'maxAge',
|
||||
'max_object_size': 'maxObjectSize',
|
||||
'process_vary': 'processVary',
|
||||
'max_secondary_entries': 'maxSecondaryEntries',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + ['enabled']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'process_vary'],
|
||||
'int': ['total_max_size', 'max_age', 'max_object_size', 'max_secondary_entries'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'total_max_size': {'min': 1, 'max': 4095},
|
||||
'max_age': {'min': 1, 'max': 3600},
|
||||
'max_object_size': {'min': 1, 'max': 2146435072},
|
||||
'max_secondary_entries': {'min': 1},
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralDefaults(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.defaults'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'max_connections': 'maxConnections',
|
||||
'max_connections_servers': 'maxConnectionsServers',
|
||||
'timeout_client': 'timeoutClient',
|
||||
'timeout_connect': 'timeoutConnect',
|
||||
'timeout_check': 'timeoutCheck',
|
||||
'timeout_server': 'timeoutServer',
|
||||
'custom_options': 'customOptions'
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + ['retries', 'redispatch', 'init_addr' ]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'int': ['max_connections', 'max_connections_servers', 'retries'],
|
||||
'select': ['redispatch'],
|
||||
'list': ['init_addr'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'max_connections': {'min': 0, 'max': 10000000},
|
||||
'max_connections_servers': {'min': 0, 'max': 10000000},
|
||||
'retries': {'min': 0, 'max': 100},
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralLogging(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.logging'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['host', 'facility', 'level', 'length']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'select': ['facility', 'level'],
|
||||
'int': ['length'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'length': {'min': 64, 'max': 65535},
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralPeers(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.peers'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name1', 'listen1', 'port1', 'name2', 'listen2', 'port2']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'int': ['port1', 'port2'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'port1': {'min': 1, 'max': 65535},
|
||||
'port2': {'min': 1, 'max': 65535},
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralSettings(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'graceful_stop': 'gracefulStop',
|
||||
'hard_stop_after': 'hardStopAfter',
|
||||
'close_spread_time': 'closeSpreadTime',
|
||||
'seamless_reload': 'seamlessReload',
|
||||
'show_intro': 'showIntro',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + ['enabled']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'graceful_stop', 'seamless_reload', 'show_intro'],
|
||||
'int': ['hard_stop_after', 'close_spread_time'],
|
||||
}
|
||||
|
||||
FIELDS_BOOL_INVERT = ['graceful_stop']
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'hard_stop_after': {'min': 0, 'max': 86400}, # 0 to 24 hours in seconds
|
||||
'close_spread_time': {'min': 0, 'max': 3600}, # 0 to 1 hour in seconds
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralStats(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.stats'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'remote_enabled': 'remoteEnabled',
|
||||
'remote_bind': 'remoteBind',
|
||||
'auth_enabled': 'authEnabled',
|
||||
'allowed_users': 'allowedUsers',
|
||||
'allowed_groups': 'allowedGroups',
|
||||
'custom_options': 'customOptions'
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys())
|
||||
FIELDS_CHANGE += ['enabled', 'port', 'users', 'prometheus_enabled', 'prometheus_bind', 'prometheus_path']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'remote_enabled', 'auth_enabled', 'prometheus_enabled'],
|
||||
'int': ['port'],
|
||||
'list': ['remote_bind', 'users', 'allowed_users', 'allowed_groups', 'prometheus_bind'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'port': {'min': 1024, 'max': 65535},
|
||||
}
|
||||
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_users': 'haproxy.users.user',
|
||||
'existing_groups': 'haproxy.groups.group',
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.existing_users = {}
|
||||
self.existing_groups = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
self.b.find_multiple_links(
|
||||
field='allowed_users',
|
||||
existing=self.existing_users,
|
||||
existing_field_id='name',
|
||||
)
|
||||
self.b.find_multiple_links(
|
||||
field='allowed_groups',
|
||||
existing=self.existing_groups,
|
||||
existing_field_id='name',
|
||||
)
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyGeneralTuning(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.general.tuning'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'max_connections': 'maxConnections',
|
||||
'resolvers_prefer': 'resolversPrefer',
|
||||
'ssl_server_verify': 'sslServerVerify',
|
||||
'max_dh_size': 'maxDHSize',
|
||||
'buffer_size': 'bufferSize',
|
||||
'spread_checks': 'spreadChecks',
|
||||
'bogus_proxy_enabled': 'bogusProxyEnabled',
|
||||
'lua_max_mem': 'luaMaxMem',
|
||||
'custom_options': 'customOptions',
|
||||
'ocsp_update_enabled': 'ocspUpdateEnabled',
|
||||
'ocsp_update_min_delay': 'ocspUpdateMinDelay',
|
||||
'ocsp_update_max_delay': 'ocspUpdateMaxDelay',
|
||||
'ssl_defaults_enabled': 'ssl_defaultsEnabled',
|
||||
'ssl_bind_options': 'ssl_bindOptions',
|
||||
'ssl_min_version': 'ssl_minVersion',
|
||||
'ssl_max_version': 'ssl_maxVersion',
|
||||
'ssl_cipher_list': 'ssl_cipherList',
|
||||
'ssl_cipher_suites': 'ssl_cipherSuites',
|
||||
'h2_initial_window_size': 'h2_initialWindowSize',
|
||||
'h2_initial_window_size_outgoing': 'h2_initialWindowSizeOutgoing',
|
||||
'h2_initial_window_size_incoming': 'h2_initialWindowSizeIncoming',
|
||||
'h2_max_concurrent_streams': 'h2_maxConcurrentStreams',
|
||||
'h2_max_concurrent_streams_outgoing': 'h2_maxConcurrentStreamsOutgoing',
|
||||
'h2_max_concurrent_streams_incoming': 'h2_maxConcurrentStreamsIncoming',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys()) + ['root', 'nbthread']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['root', 'bogus_proxy_enabled', 'ocsp_update_enabled', 'ssl_defaults_enabled'],
|
||||
'int': ['max_connections', 'nbthread', 'max_dh_size', 'buffer_size', 'spread_checks',
|
||||
'lua_max_mem', 'ocsp_update_min_delay', 'ocsp_update_max_delay',
|
||||
'h2_initial_window_size', 'h2_initial_window_size_outgoing',
|
||||
'h2_initial_window_size_incoming', 'h2_max_concurrent_streams',
|
||||
'h2_max_concurrent_streams_outgoing', 'h2_max_concurrent_streams_incoming'],
|
||||
'select': ['resolvers_prefer', 'ssl_server_verify', 'ssl_min_version', 'ssl_max_version'],
|
||||
'list': ['ssl_bind_options'],
|
||||
}
|
||||
|
||||
INT_VALIDATIONS = {
|
||||
'max_connections': {'min': 0, 'max': 10000000},
|
||||
'nbthread': {'min': 1, 'max': 1024},
|
||||
'max_dh_size': {'min': 1024, 'max': 16384},
|
||||
'buffer_size': {'min': 1024, 'max': 1048576},
|
||||
'spread_checks': {'min': 0, 'max': 50},
|
||||
'lua_max_mem': {'min': 0, 'max': 1024},
|
||||
'ocsp_update_min_delay': {'min': 1, 'max': 86400},
|
||||
'ocsp_update_max_delay': {'min': 1, 'max': 86400},
|
||||
'h2_initial_window_size': {'min': 0, 'max': 10000000},
|
||||
'h2_initial_window_size_outgoing': {'min': 0, 'max': 10000000},
|
||||
'h2_initial_window_size_incoming': {'min': 0, 'max': 10000000},
|
||||
'h2_max_concurrent_streams': {'min': 0, 'max': 10000000},
|
||||
'h2_max_concurrent_streams_outgoing': {'min': 0, 'max': 10000000},
|
||||
'h2_max_concurrent_streams_incoming': {'min': 0, 'max': 10000000},
|
||||
}
|
||||
|
||||
TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyGroup(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addGroup',
|
||||
'del': 'delGroup',
|
||||
'set': 'setGroup',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleGroup',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.groups.group'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name', 'description', 'members', 'add_userlist']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
EXIST_ATTR = 'haproxy_group'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'add_userlist'],
|
||||
'list': ['members']
|
||||
}
|
||||
|
||||
SEARCH_ADDITIONAL = {
|
||||
'existing_users': 'haproxy.users.user',
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_group = {}
|
||||
self.existing_users = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._base_check()
|
||||
|
||||
if self.p['state'] == 'present':
|
||||
self.b.find_multiple_links(
|
||||
field='members',
|
||||
existing=self.existing_users,
|
||||
existing_field_id='name',
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyLua(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addLua',
|
||||
'del': 'delLua',
|
||||
'set': 'setLua',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleLua',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.luas.lua'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name', 'description', 'preload', 'filename_scheme', 'content']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
EXIST_ATTR = 'haproxy_lua'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'preload'],
|
||||
'select': ['filename_scheme'],
|
||||
}
|
||||
|
||||
STR_VALIDATIONS = {
|
||||
'name': r'^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$',
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_lua = {}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class HaproxyMaintenance(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.maintenance.cronjobs'
|
||||
API_KEY_PATH_REQ = API_KEY_PATH
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
|
||||
FIELDS_TRANSLATE = {
|
||||
'sync_certs': 'syncCerts',
|
||||
'reload_service': 'reloadService',
|
||||
'restart_service': 'restartService',
|
||||
}
|
||||
|
||||
FIELDS_CHANGE = list(FIELDS_TRANSLATE.keys())
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['sync_certs', 'reload_service', 'restart_service'],
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
|
||||
|
||||
class HaproxyUser(BaseModule):
|
||||
FIELD_ID = 'name'
|
||||
|
||||
CMDS = {
|
||||
'add': 'addUser',
|
||||
'del': 'delUser',
|
||||
'set': 'setUser',
|
||||
'search': 'get',
|
||||
'toggle': 'toggleUser',
|
||||
}
|
||||
API_KEY_PATH = 'haproxy.users.user'
|
||||
API_MOD = 'haproxy'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reconfigure'
|
||||
|
||||
FIELDS_CHANGE = ['enabled', 'name', 'description', 'password']
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
|
||||
FIELDS_DIFF_NO_LOG = ['password']
|
||||
|
||||
EXIST_ATTR = 'haproxy_user'
|
||||
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
}
|
||||
|
||||
TIMEOUT = 20.0
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session)
|
||||
self.haproxy_user = {}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY_PATH = 'hasync'
|
||||
API_MOD = 'core'
|
||||
API_CONT = 'hasync'
|
||||
FIELDS_CHANGE = [
|
||||
'preempt', 'disconnect_ppps', 'pfsync_interface', 'pfsync_peer_ip', 'pfsync_version', 'synchronize_to_ip',
|
||||
'verify_peer', 'username', 'syncitems',
|
||||
]
|
||||
FIELDS_ALL = ['password']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'preempt': 'disablepreempt',
|
||||
'disconnect_ppps': 'disconnectppps',
|
||||
'pfsync_interface': 'pfsyncinterface',
|
||||
'pfsync_peer_ip': 'pfsyncpeerip',
|
||||
'pfsync_version': 'pfsyncversion',
|
||||
'synchronize_to_ip': 'synchronizetoip',
|
||||
'verify_peer': 'verifypeer',
|
||||
}
|
||||
FIELDS_BOOL_INVERT = ['preempt']
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['preempt', 'disconnect_ppps', 'verify_peer'],
|
||||
'select': ['pfsync_interface', 'pfsync_version'],
|
||||
'list': ['syncitems'],
|
||||
}
|
||||
FIELDS_DIFF_NO_LOG = ['password']
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
||||
def check(self) -> None:
|
||||
if self.p['update_password'] == 'always':
|
||||
self.FIELDS_CHANGE = self.FIELDS_CHANGE + ['password']
|
||||
|
||||
self._base_check()
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import GeneralModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
get_selected, get_key_by_value_from_selection
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_ip_or_network, is_unset
|
||||
|
||||
|
||||
class General(GeneralModule):
|
||||
CMDS = {
|
||||
'set': 'set',
|
||||
'search': 'get',
|
||||
}
|
||||
API_KEY = 'general'
|
||||
API_KEY_1 = 'ids'
|
||||
API_KEY_PATH = f'{API_KEY_1}.{API_KEY}'
|
||||
API_MOD = 'ids'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = [
|
||||
'block', 'promiscuous', 'enabled', 'interfaces', 'pattern_matcher', 'local_networks', 'default_packet_size',
|
||||
'syslog_alerts', 'syslog_output', 'log_level', 'log_rotate', 'log_retention', 'log_payload',
|
||||
'profile', 'profile_toclient_groups', 'profile_toserver_groups', 'schedule',
|
||||
]
|
||||
FIELDS_ALL = FIELDS_CHANGE
|
||||
FIELDS_TRANSLATE = {
|
||||
'block': 'ips',
|
||||
'promiscuous': 'promisc',
|
||||
'syslog_alerts': 'syslog',
|
||||
'syslog_output': 'syslog_eve',
|
||||
'log_level': 'verbosity',
|
||||
'pattern_matcher': 'MPMAlgo',
|
||||
'local_networks': 'homenet',
|
||||
'default_packet_size': 'defaultPacketSize',
|
||||
'log_rotate': 'AlertLogrotate',
|
||||
'log_retention': 'AlertSaveLogs',
|
||||
'log_payload': 'LogPayload',
|
||||
'schedule': 'UpdateCron',
|
||||
}
|
||||
FIELDS_TRANSLATE_SPECIAL = {
|
||||
'profile': 'Profile',
|
||||
'profile_toclient_groups': 'toclient_groups',
|
||||
'profile_toserver_groups': 'toserver_groups',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled', 'block', 'promiscuous', 'syslog_alerts', 'syslog_output', 'log_payload'],
|
||||
'int': ['default_packet_size', 'log_retention'],
|
||||
'list': ['local_networks', 'interfaces'],
|
||||
'select': ['log_level', 'pattern_matcher', 'log_rotate', 'schedule'],
|
||||
}
|
||||
FIELDS_IGNORE = ['detect']
|
||||
INT_VALIDATIONS = {
|
||||
'log_retention': {'min': 1, 'max': 1000},
|
||||
'profile_toclient_groups': {'min': 1, 'max': 65535},
|
||||
'profile_toserver_groups': {'min': 1, 'max': 65535},
|
||||
'default_packet_size': {'min': 82, 'max': 65535},
|
||||
}
|
||||
FIELDS_VALUE_MAPPING = {
|
||||
'log_rotate': {
|
||||
'weekly': 'W0D23',
|
||||
'daily': 'D0',
|
||||
},
|
||||
'log_level': {
|
||||
'info': 'v',
|
||||
'perf': 'vv',
|
||||
'config': 'vvv',
|
||||
'debug': 'vvvv',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
|
||||
GeneralModule.__init__(self=self, m=module, r=result, s=session)
|
||||
|
||||
def check(self) -> None:
|
||||
if len(self.p['interfaces']) == 0:
|
||||
self.m.fail_json("You need to supply 'interfaces'!")
|
||||
|
||||
if self.p['profile'] == 'custom' and (
|
||||
is_unset(self.p['profile_toclient_groups']) or is_unset(self.p['profile_toserver_groups'])
|
||||
):
|
||||
self.m.fail_json(
|
||||
"You need to supply 'profile_toclient_groups' and 'profile_toserver_groups' "
|
||||
"when using the profile 'custom'!"
|
||||
)
|
||||
|
||||
for net in self.p['local_networks']:
|
||||
if not is_ip_or_network(net):
|
||||
self.m.fail_json(
|
||||
f"It seems you provided an invalid network in 'local_networks': '{net}'"
|
||||
)
|
||||
|
||||
self._base_check()
|
||||
|
||||
def _search_call(self) -> dict:
|
||||
settings = self.s.get(cnf={
|
||||
**self.call_cnf, **{'command': self.CMDS['search']}
|
||||
})[self.API_KEY_1][self.API_KEY]
|
||||
|
||||
simple = self.b.simplify_existing(settings)
|
||||
|
||||
try:
|
||||
# resolve schedule/cron name to uuid
|
||||
self.p['schedule'] = get_key_by_value_from_selection(
|
||||
selection=settings[self.FIELDS_TRANSLATE['schedule']],
|
||||
value=self.p['schedule'],
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
# list module not supplying params
|
||||
pass
|
||||
|
||||
simple['profile'] = get_selected(
|
||||
settings['detect'][self.FIELDS_TRANSLATE_SPECIAL['profile']]
|
||||
)
|
||||
simple['profile_toclient_groups'] = settings['detect'][self.FIELDS_TRANSLATE_SPECIAL['profile_toclient_groups']]
|
||||
simple['profile_toserver_groups'] = settings['detect'][self.FIELDS_TRANSLATE_SPECIAL['profile_toserver_groups']]
|
||||
|
||||
for field in self.FIELDS_IGNORE:
|
||||
if field in simple:
|
||||
simple.pop(field)
|
||||
|
||||
return simple
|
||||
|
||||
def _build_request(self) -> dict:
|
||||
raw_request = self.b.build_request(
|
||||
ignore_fields=['profile', 'profile_toclient_groups', 'profile_toserver_groups']
|
||||
)
|
||||
raw_request[self.API_KEY]['detect'] = {
|
||||
self.FIELDS_TRANSLATE_SPECIAL['profile']: self.p['profile'],
|
||||
self.FIELDS_TRANSLATE_SPECIAL['profile_toclient_groups']: self.p['profile_toclient_groups'],
|
||||
self.FIELDS_TRANSLATE_SPECIAL['profile_toserver_groups']: self.p['profile_toserver_groups'],
|
||||
}
|
||||
|
||||
return {self.API_KEY_1: raw_request}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.main.ids_ruleset import Ruleset
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.validate import \
|
||||
is_unset, is_true, ensure_list
|
||||
|
||||
|
||||
class Policy(BaseModule):
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'add': 'add_policy',
|
||||
'del': 'del_policy',
|
||||
'set': 'set_policy',
|
||||
'search': 'search_policy',
|
||||
'detail': 'get_policy',
|
||||
'toggle': 'toggle_policy',
|
||||
}
|
||||
API_KEY = 'policy'
|
||||
API_KEY_PATH = f'policies.{API_KEY}'
|
||||
API_MOD = 'ids'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['priority', 'action', 'rulesets', 'new_action']
|
||||
FIELDS_ALL = ['enabled', FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TRANSLATE = {
|
||||
'priority': 'prio',
|
||||
}
|
||||
FIELDS_TRANSLATE_SPECIAL = {
|
||||
'rules': 'content',
|
||||
}
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['new_action'],
|
||||
'list': ['rulesets', 'action'],
|
||||
'int': ['priority'],
|
||||
}
|
||||
EXIST_ATTR = 'policy'
|
||||
QUERY_MAX_RULES = 5000
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.policy = {}
|
||||
self.exists = False
|
||||
self.enabled_rulesets = {}
|
||||
self.ruleset_names = {}
|
||||
|
||||
def check(self) -> None:
|
||||
self._search_call()
|
||||
if self.p['state'] == 'present' and not is_unset(self.p['rulesets']):
|
||||
if len(self.enabled_rulesets) == 0:
|
||||
self._search_rulesets()
|
||||
|
||||
if len(self.enabled_rulesets) == 0:
|
||||
self.m.fail_json("You need to enable rulesets before referencing them!")
|
||||
|
||||
ruleset_uuids = []
|
||||
for ruleset in self.p['rulesets']:
|
||||
found = False
|
||||
for enabled_ruleset, uuid in self.enabled_rulesets.items():
|
||||
if enabled_ruleset == ruleset:
|
||||
found = True
|
||||
ruleset_uuids.append(uuid)
|
||||
|
||||
if not found:
|
||||
self.m.fail_json(
|
||||
f"The ruleset '{ruleset}' was not found! "
|
||||
"You need to enable a ruleset before referencing it. "
|
||||
f"Enabled ones are: {list(self.enabled_rulesets.keys())}"
|
||||
)
|
||||
|
||||
ruleset_uuids.sort()
|
||||
self.p['rulesets'] = ruleset_uuids
|
||||
|
||||
self.r['diff']['after'] = self.b.build_diff(data=self.p)
|
||||
|
||||
def get_existing(self) -> list:
|
||||
return self._search_call()
|
||||
|
||||
def _search_call(self) -> list:
|
||||
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
|
||||
# https://github.com/opnsense/core/issues/7094
|
||||
existing = self.s.post(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['search'],
|
||||
'data': {'current': 1, 'rowCount': self.QUERY_MAX_RULES, 'sort': self.FIELD_ID, 'searchPhrase': ''},
|
||||
})['rows']
|
||||
|
||||
if self.FIELD_ID in self.p: # list module
|
||||
for policy in existing:
|
||||
if policy[self.FIELD_ID] == self.p[self.FIELD_ID]:
|
||||
self.exists = True
|
||||
self.call_cnf['params'] = [policy['uuid']]
|
||||
raw_policy = self.s.get(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['detail'],
|
||||
})[self.API_KEY]
|
||||
self.policy['rules'] = self._parse_rules(raw_policy)
|
||||
self.policy = self.b.simplify_existing(raw_policy)
|
||||
self.enabled_rulesets = self._format_ruleset(raw_policy['rulesets'])
|
||||
self.policy['uuid'] = policy['uuid']
|
||||
if 'content' in self.policy:
|
||||
self.policy.pop('content')
|
||||
|
||||
self.r['diff']['before'] = self.policy
|
||||
|
||||
return existing
|
||||
|
||||
@staticmethod
|
||||
def _parse_rules(raw_policy: dict) -> dict:
|
||||
parsed = {}
|
||||
|
||||
if 'content' not in raw_policy:
|
||||
return parsed
|
||||
|
||||
for key_value, values in raw_policy['content'].items():
|
||||
if is_true(values['selected']):
|
||||
key, value = key_value.split('.', 1)
|
||||
if key in parsed:
|
||||
parsed[key].append(value)
|
||||
else:
|
||||
parsed[key] = [value]
|
||||
|
||||
return parsed
|
||||
|
||||
def _build_request(self) -> dict:
|
||||
raw_request = self.b.build_request(ignore_fields=['rules'])
|
||||
|
||||
# formatting dynamic rules
|
||||
# example: 'policy_content_affected_product: "affected_product.Adobe_Flash,affected_product.Adobe_Reader"'
|
||||
raw_request_rules = {}
|
||||
raw_request_content = []
|
||||
for key, values in self.p['rules'].items():
|
||||
fmt_values = [f'{key}.{value}' for value in ensure_list(values)]
|
||||
raw_request_rules[f'policy_content_{key}'] = self.b.RESP_JOIN_CHAR.join(fmt_values)
|
||||
raw_request_content.extend(fmt_values)
|
||||
|
||||
raw_request[self.API_KEY]['content'] = self.b.RESP_JOIN_CHAR.join(raw_request_content)
|
||||
|
||||
return {
|
||||
**raw_request,
|
||||
**raw_request_rules,
|
||||
}
|
||||
|
||||
def _search_rulesets(self):
|
||||
# check if any ruleset is enabled before creating a new policy
|
||||
self.enabled_rulesets = self._format_ruleset(
|
||||
self.s.get(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['detail'],
|
||||
})[self.API_KEY]['rulesets']
|
||||
)
|
||||
|
||||
def _search_ruleset_names(self):
|
||||
ruleset_details = self.s.get(cnf={
|
||||
**self.call_cnf,
|
||||
'command': Ruleset.CMDS['search'],
|
||||
})['rows']
|
||||
|
||||
for ruleset in ruleset_details:
|
||||
self.ruleset_names[ruleset[Ruleset.FIELD_PK]] = ruleset[Ruleset.FIELD_ID]
|
||||
|
||||
def _format_ruleset(self, rulesets: dict) -> dict:
|
||||
if len(self.ruleset_names) == 0:
|
||||
self._search_ruleset_names()
|
||||
|
||||
formatted = {}
|
||||
|
||||
for uuid, ruleset in rulesets.items():
|
||||
formatted[self.ruleset_names[ruleset['value']]] = uuid
|
||||
|
||||
return formatted
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_true
|
||||
|
||||
|
||||
class Rule(BaseModule):
|
||||
FIELD_ID = 'sid'
|
||||
CMDS = {
|
||||
'add': 'add_policy_rule',
|
||||
'del': 'del_policy_rule',
|
||||
'set': 'set_policy_rule',
|
||||
'search': 'search_policy_rule',
|
||||
'detail': 'get_policy_rule',
|
||||
'toggle': 'toggle_policy_rule',
|
||||
}
|
||||
API_KEY_PATH = 'policies.rule'
|
||||
API_MOD = 'ids'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
FIELDS_CHANGE = ['action']
|
||||
FIELDS_ALL = ['enabled', FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
'int': ['sid'],
|
||||
}
|
||||
EXIST_ATTR = 'rule'
|
||||
QUERY_MAX_RULES = 1000
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.rule = {}
|
||||
self.exists = False
|
||||
|
||||
def check(self) -> None:
|
||||
self.r['diff'] = {'before': {}, 'after': {}}
|
||||
self._search_call()
|
||||
self.r['diff']['after'] = self.b.build_diff(data=self.p)
|
||||
self.r['changed'] = self.r['diff']['before'] != self.r['diff']['after']
|
||||
|
||||
def _search_call(self) -> list:
|
||||
existing = self.s.post(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['search'],
|
||||
'data': {'current': 1, 'rowCount': self.QUERY_MAX_RULES, 'sort': self.FIELD_ID},
|
||||
})['rows']
|
||||
|
||||
if self.FIELD_ID in self.p: # list module
|
||||
for rule in existing:
|
||||
if int(rule[self.FIELD_ID]) == self.p[self.FIELD_ID]:
|
||||
self.exists = True
|
||||
self.rule['uuid'] = rule['uuid']
|
||||
self.call_cnf['params'] = [self.rule['uuid']]
|
||||
self.rule[self.FIELD_ID] = int(rule[self.FIELD_ID])
|
||||
self.rule['enabled'] = is_true(rule['enabled'])
|
||||
self.rule['action'] = rule['action'].lower()
|
||||
self.r['diff']['before'] = self.rule
|
||||
break
|
||||
|
||||
return existing
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
to_digit
|
||||
|
||||
|
||||
class Rule(BaseModule):
|
||||
FIELD_PK = 'sid'
|
||||
CMDS = {
|
||||
'set': 'setRule',
|
||||
'search': 'searchinstalledrules',
|
||||
'toggle': 'toggleRule',
|
||||
}
|
||||
API_KEY_PATH = 'rules.rule'
|
||||
API_MOD = 'ids'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'reloadRules'
|
||||
FIELDS_CHANGE = ['action']
|
||||
FIELDS_ALL = ['enabled']
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
'select': ['action'],
|
||||
'int': ['sid'],
|
||||
}
|
||||
EXIST_ATTR = 'rule'
|
||||
QUERY_MAX_RULES = 5000
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.rule = {}
|
||||
self.exists = False
|
||||
|
||||
def check(self) -> None:
|
||||
self._search_call()
|
||||
if not self.exists:
|
||||
self.m.fail_json(f"The provided rule '{self.p[self.FIELD_PK]}' was not found!")
|
||||
|
||||
self.r['diff']['after'] = self.b.build_diff(data=self.p)
|
||||
self.r['changed'] = self.r['diff']['before'] != self.r['diff']['after']
|
||||
|
||||
def process(self) -> None:
|
||||
if self.rule['action'] != self.p['action']:
|
||||
self.update()
|
||||
|
||||
if self.rule['enabled'] != self.p['enabled']:
|
||||
self.toggle()
|
||||
|
||||
def _search_call(self) -> list:
|
||||
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
|
||||
# https://github.com/opnsense/core/issues/7094
|
||||
existing = self.s.post(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['search'],
|
||||
'data': {'current': 1, 'rowCount': self.QUERY_MAX_RULES, 'sort': self.FIELD_PK},
|
||||
})['rows']
|
||||
|
||||
if self.FIELD_PK in self.p: # list module
|
||||
for rule in existing:
|
||||
if rule[self.FIELD_PK] == self.p[self.FIELD_PK]:
|
||||
self.exists = True
|
||||
self.rule[self.FIELD_PK] = rule[self.FIELD_PK]
|
||||
self.rule['enabled'] = rule['status'] == 'enabled'
|
||||
self.rule['action'] = rule['action']
|
||||
self.r['diff']['before'] = self.rule
|
||||
|
||||
return existing
|
||||
|
||||
def toggle(self) -> None:
|
||||
if not self.m.check_mode:
|
||||
self.s.post(cnf={
|
||||
**self.call_cnf, **{
|
||||
'command': self.CMDS['toggle'],
|
||||
'params': [self.p[self.FIELD_PK], to_digit(self.p['enabled'])],
|
||||
}
|
||||
})
|
||||
|
||||
def update(self) -> None:
|
||||
if not self.m.check_mode:
|
||||
self.s.post(cnf={
|
||||
**self.call_cnf, **{
|
||||
'command': self.CMDS['set'],
|
||||
'params': [self.p[self.FIELD_PK]],
|
||||
'data': {'action': self.p['action']}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.api import \
|
||||
Session
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.base.cls import BaseModule
|
||||
from ansible_collections.oxlorg.opnsense.plugins.module_utils.helper.main import \
|
||||
is_true, to_digit
|
||||
|
||||
|
||||
class Ruleset(BaseModule):
|
||||
FIELD_PK = 'filename'
|
||||
FIELD_ID = 'description'
|
||||
CMDS = {
|
||||
'set': 'set_ruleset',
|
||||
'search': 'list_rulesets',
|
||||
'toggle': 'toggle_ruleset',
|
||||
}
|
||||
API_KEY_PATH = 'rulesets.ruleset'
|
||||
API_MOD = 'ids'
|
||||
API_CONT = 'settings'
|
||||
API_CONT_REL = 'service'
|
||||
API_CMD_REL = 'updateRules'
|
||||
FIELDS_CHANGE = ['enabled']
|
||||
FIELDS_COPY = [FIELD_PK, 'documentation_url']
|
||||
FIELDS_ALL = [FIELD_ID]
|
||||
FIELDS_ALL.extend(FIELDS_COPY)
|
||||
FIELDS_ALL.extend(FIELDS_CHANGE)
|
||||
FIELDS_TYPING = {
|
||||
'bool': ['enabled'],
|
||||
}
|
||||
EXIST_ATTR = 'ruleset'
|
||||
QUERY_MAX_RULES = 1000
|
||||
|
||||
def __init__(self, module: AnsibleModule, result: dict, session: Session = None, fail: dict = None):
|
||||
BaseModule.__init__(self=self, m=module, r=result, s=session, f=fail)
|
||||
self.ruleset = {}
|
||||
self.exists = False
|
||||
self.existing_rulesets_desc = []
|
||||
|
||||
def check(self) -> None:
|
||||
self._search_call()
|
||||
if not self.exists:
|
||||
self.m.fail_json(
|
||||
f"The provided ruleset '{self.p[self.FIELD_ID]}' was not found! "
|
||||
f"Available ones are: '{self.existing_rulesets_desc}'"
|
||||
)
|
||||
|
||||
self.r['diff']['after'] = self.b.build_diff(data=self.p)
|
||||
self.r['changed'] = self.r['diff']['before'] != self.r['diff']['after']
|
||||
|
||||
def process(self) -> None:
|
||||
if self.r['changed']:
|
||||
self.toggle()
|
||||
|
||||
def _search_call(self) -> list:
|
||||
# NOTE: workaround for issue with incomplete response-data from 'get' endpoint:
|
||||
# https://github.com/opnsense/core/issues/7094
|
||||
existing = self.s.post(cnf={
|
||||
**self.call_cnf,
|
||||
'command': self.CMDS['search'],
|
||||
'data': {'current': 1, 'rowCount': self.QUERY_MAX_RULES, 'sort': self.FIELD_PK, 'searchPhrase': ''},
|
||||
})['rows']
|
||||
|
||||
if self.FIELD_ID in self.p: # list module
|
||||
for ruleset in existing:
|
||||
self.existing_rulesets_desc.append(ruleset[self.FIELD_ID])
|
||||
|
||||
if ruleset[self.FIELD_ID] == self.p[self.FIELD_ID]:
|
||||
self.exists = True
|
||||
for field in self.FIELDS_COPY:
|
||||
self.ruleset[field] = ruleset[field]
|
||||
self.p[field] = ruleset[field]
|
||||
|
||||
self.ruleset[self.FIELD_ID] = ruleset[self.FIELD_ID]
|
||||
self.ruleset['enabled'] = is_true(ruleset['enabled'])
|
||||
self.r['diff']['before'] = self.ruleset
|
||||
|
||||
return existing
|
||||
|
||||
def toggle(self) -> None:
|
||||
if not self.m.check_mode:
|
||||
self.s.post(cnf={
|
||||
**self.call_cnf, **{
|
||||
'command': self.CMDS['toggle'],
|
||||
'params': [self.p[self.FIELD_PK], to_digit(self.p['enabled'])],
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue