pax_global_header 0000666 0000000 0000000 00000000064 14200534377 0014517 g ustar 00root root 0000000 0000000 52 comment=d6035fe2c490c801a1b0c2774f93df7f0dbfd136
freeipa-healthcheck-0.10/ 0000775 0000000 0000000 00000000000 14200534377 0015313 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/.github/ 0000775 0000000 0000000 00000000000 14200534377 0016653 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/.github/workflows/ 0000775 0000000 0000000 00000000000 14200534377 0020710 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/.github/workflows/pipelines.yml 0000664 0000000 0000000 00000003476 14200534377 0023435 0 ustar 00root root 0000000 0000000 name: CI
on:
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Lint with flake8
run: |
tox -vv -eflake8
- name: Lint with pep8
run: |
tox -vv -epep8
container:
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
fedora-release: [33, 34]
steps:
- uses: actions/checkout@v2
- name: Lint with pylint fedora:${{ matrix.fedora-release }}
run: |
docker pull fedora:${{ matrix.fedora-release }}
docker run \
-v ${GITHUB_WORKSPACE}:/root/src/ fedora:${{ matrix.fedora-release }} \
/bin/bash -c "\
dnf -y install \
freeipa-server \
freeipa-server-trust-ad \
tox \
python3-pylint \
python3-pytest \
; \
cd /root/src; \
tox -vv -elint; \
"
- name: pytest fedora:${{ matrix.fedora-release }}
run: |
docker pull fedora:${{ matrix.fedora-release }}
docker run \
-v ${GITHUB_WORKSPACE}:/root/src/ fedora:${{ matrix.fedora-release }} \
/bin/bash -c "\
dnf -y install \
freeipa-server \
freeipa-server-trust-ad \
tox \
python3-pylint \
python3-pytest \
; \
cd /root/src; \
tox -vv -epy3; \
"
freeipa-healthcheck-0.10/.gitignore 0000664 0000000 0000000 00000000075 14200534377 0017305 0 ustar 00root root 0000000 0000000 *.pyc
*.egg-info
/venv
*,cover
.coverage
.pytest_cache
.tox
freeipa-healthcheck-0.10/COPYING 0000664 0000000 0000000 00000104513 14200534377 0016352 0 ustar 00root root 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
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
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This 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 .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
freeipa-healthcheck-0.10/README.md 0000664 0000000 0000000 00000076053 14200534377 0016605 0 ustar 00root root 0000000 0000000 # What is healthcheck?
It is an attempt to answer the question "Is my IPA installation working properly."
Major pain points in an IPA installation were identified and tests written to verify that the system is configured or running with expected settings.
The major areas currently covered are:
* Certificate configuration and expiration dates
* Replication errors
* Replication topology
* AD Trust configuration
* Service status
* File permissions of important configuration files
* File system space
# How to use it?
The simplest way to use Healthcheck is to run it from the command-line as root as ipa-healthcheck. Running from the command-line will display the output to the console unless --output-file=FILENAME is used.
There is output for _all_ tests so we can be sure that an error condition isn't providing a false positive. The command-line option --failures-only will skip printing the SUCCESS conditions. If running in a tty and not using --output-file then --failures-only defaults to True. The --all option will display all output if you want/need it.
To automate running Healthcheck every day a systemd timer can be used.
The default destination directory for healthcheck logs is `/var/log/ipa/healthcheck` and this can be the input into a monitoring system to track changes over time or to alert if a test goes from working to error or warning.
A systemd timer is provided but is not enabled by default. To enable it:
# systemctl enable ipa-healthcheck.timer
# systemctl start ipa-healthcheck.timer
logrotate will handle log rotation and keep up to 30 days of history.
This can be configured via the `/etc/logrotate.d/ipahealthcheck` file.
If using upstream or if your distribution's package does not include the timer, it can be installed manually as follows.
First create the destination log directory:
# mkdir /var/log/ipa/healthcheck
Then copy the systemd configuration into place:
# cp systemd/ipa-healthcheck.timer /usr/lib/systemd/system
# cp systemd/ipa-healthcheck.service /usr/lib/systemd/system
Put a shell script in place to do the invocation:
# cp systemd/ipa-healthcheck.sh /usr/libexec/ipa
Tell systemd about it and enable it:
# systemctl daemon-reload
# systemctl enable ipa-healthcheck.timer
# systemctl start ipa-healthcheck.timer
Finally add a proper logrotate configuration:
# cp logrotate/ipahealthcheck /etc/logrotate.d/
Note that logrotate requires crond to be started+enabled.
To test:
# systemctl start ipa-healthcheck
# What if I get an error or warning?
In general the output should contain enough information to provide a basic idea of why it is considered an error. If a specific value is expected then that will be provided along with the observed value. For example a number of files are checked for owner, group and permissions. If a value differs from the expected value then the expected and got values will be reported.
Running from the command-line will aid in ensuring that the condition is correct to what is expected. The basic idea is that it would be iterative:
1. ipa-healthcheck
2. manually address any errors
Repeat until until no errors are reported.
# What about false positives?
It is possible that some tests will need to be tweaked to accommodate real world situations. If you observe false positives then please open an issue at [https://github.com/freeipa/freeipa-healthcheck/issues](URL)
There is no way to suppress an error without making a change either in the test or in the system to accommodate the test requirements.
# Organization
In order to gauge the health of a system one needs to check any number of things.
These things, or checks, can be logically grouped together. This is a source. A source consists of 1..n checks.
A check should be as atomic as possible to limit the scope and complexity, ideally returning a yes/no whether the check passes or fails. This is not always possible and that's ok.
At a higher level than source is product. The hierarchy looks like:
ipahealthcheck
product
source
check
check
...
source
check
...
A source provides a registry so its checks are discoverable.
# Writing a check module
The base class for a check is ipahealthcheck.core.plugin::Plugin
The only method that needs to be implemented is check(). This implements the test against the system and should yield a Result object. Because check() is a generator multiple results can be yielded from a single check.
Typically each source defines its own plugin.py which contains the registry. This looks like:
from ipahealthcheck.core.plugin import Registry
registry = Registry()
A basic check module consists of:
from ipahealthcheck.core.plugin import Plugin, Result
from ipahealthcheck.core import constants
from ipahealthcheck.mymodule.plugin import registry
@registry
class MyPlugin(Plugin):
def check(self):
yield Result(self, constants.SUCCESS)
# Return value
A check yields a Result. This contains the outcome of the check including:
* result as defined in ipahealthcheck/core/constants.py
* kw, a python dictionary of name value pairs that provide details on the error
The kw dict is meant to provide context for the check. Err on the side of
too much information.
Some predefined keys of the kw dictionary are:
* key: some checks can have multiple tests. This provides for uniqueness.
* msg: A message that can take other keywords as input
* exception: used when a check raises an exception
kw is optional if result is SUCCESS.
If a check consist of only a single test then it is not required to yield
a Result, one marking the check as successful will be added automatically.
If a check is complex enough that it checks multiple values then it should
yield a SUCCESS Result() for each one.
A Result is required for every test done so that one can know that the
check was executed.
The run time duration of each check will be calculated. The mechanism
differs depending on complexity.
A check should normally use the @duration decorator to track the
duration it took to execute the check.
@registry
class MyPlugin(Plugin):
@duration
def check(self):
yield Result(self, constants.SUCCESS)
# Registering a source
The list of sources is stored in setup.py in the top-level of the tree.
Assuming it is contained in-tree it takes the form of:
'ipahealthcheck.': [
'name = ipahealthcheck..'
]
For example, to add replication to the src/ipahealthcheck/ipa directory
'ipahealthcheck.ipa': [
'ipacerts = ipahealthcheck.ipa.certs',
'ipafiles = ipahealthcheck.ipa.files',
'ipakerberos = ipahealthcheck.ipa.kerberos',
'replication = ipahealthcheck.ipa.replication',
],
If a new branch of sources is added a new registry is needed. This is
added into the ipahealthcheck.registry section in setup.py. If we decided
that replication didn't belong under ipahealthcheck.ipa but instead in
ipahealthcheck.ds it would look like:
'ipahealthcheck.registry': [
'ipahealthcheck.ipa = ipahealthcheck.ipa.plugin:registry',
'ipahealthcheck.dogtag = ipahealthcheck.dogtag.plugin:registry',
'ipahealthcheck.meta = ipahealthcheck.meta.plugin:registry',
'ipahealthcheck.ds = ipahealthcheck.ds.plugin:registry',
],
and
'ipahealthcheck.ds': [
'replication = ipahealthcheck.ds.replication',
],
# Execution
It is possible to execute a single check or all checks in a single source by passing --source and/or --check on the command-line. This is intended to help user's quickly ensure that something is fixed by re-running a check after making a change.
`--source`, when used on its own i.e. without `--check`, can also
accept a module *namespace* and all sources within that namespace
shall be executed.
# Output
Output is controlled via Output plugins. These take the global Results object and iterate over it to produce output in the desired format. The result is returned as a string.
A custom Output class must implement the generate method which generates the output.
A bare-bones output class is:
@output_registry
class Basic(Output):
def generate(self, data):
output = [x for x in data.output()]
return output
An output object can declare its own options by adding a tuple named options to the class in the form of (arg_name, dict(argparse options).
An example to provide an option to indent the text to make it more readable.
options = (
(--indent', dict(dest='indent', help='How deeply to indent')),
)
# Meta
The meta source is intended to collect basic information about the run such as the host it is run on and the time it was run.
# Useful to diagnose a failed installation?
No. healthcheck compares a known state to the state of the installation. If the installation failed then you are guaranteed to get a ton of false positives and all it will tell you is that your installation failed.
# Testing and development
The package can be tested and developed in a python virtual environment.
It requires a full freeIPA deployment so full set of system packages
need to be installed and an IPA master running.
To create the virtual environment run:
% python3 -m venv --system-site-packages venv
% venv/bin/pip install -e .
To use the environment
% source venv/bin/activate
To run the healthchecks (must be done as root for proper results):
# source venv/bin/activate
# ipa-healthcheck
To run the tests execute the virtual environment:
% pip install pytest
% pytest
The configuration file and directory are not yet created so you'll need
to do that manually:
# mkdir /etc/ipahealthcheck
# echo "[default]" > /etc/ipahealthcheck/ipahealthcheck.conf
# Understanding the results
Here is some basic guidance on what a non-SUCCESS message from a check means. How to fix any particular result is heavily dependent on the error(s) discovered and their context. A single failure may be detected by multiple checks.
## ipahealthcheck.dogtag.ca
### DogtagCertsConfigCheck
Compares the value of the CA (and KRA if installed) certificates with the value found in CS.cfg. If they don't match then the CA will likely fail to start.
{
"source": "ipahealthcheck.dogtag.ca",
"check": "DogtagCertsConfigCheck",
"result": "ERROR",
"kw": {
"key": "ocspSigningCert cert-pki-ca",
"directive": "ca.ocsp_signing.cert",
"configfile": "/var/lib/pki/pki-tomcat/conf/ca/CS.cfg",
"msg": "Certificate 'ocspSigningCert cert-pki-ca' does not match the value of ca.ocsp_signing.cert in /var/lib/pki/pki-tomcat/conf/ca/CS.cfg"
}
}
### DogtagCertsConnectivityCheck
Runs the equivalent of ipa cert-show 1 to verify basic connectivity.
{
"source": "ipahealthcheck.dogtag.ca",
"check": "DogtagCertsConnectivityCheck",
"result": "ERROR",
"kw": {
"msg": "Request for certificate failed, Certificate operation cannot be completed: Unable to communicate with CMS (503)"
}
}
## ipahealthcheck.ds.replication
### ReplicationConflictCheck
Searches for entries in LDAP matching (&(!(objectclass=nstombstone))(nsds5ReplConflict=*))
{
"source": "ipahealthcheck.ds.replication",
"check": "ReplicationConflictCheck",
"result": "ERROR",
"kw": {
"key": "nsuniqueid=66446001-1dd211b2+uid=bjenkins,cn=users,cn=accounts,dc=example,dc=test",
"conflict": "namingConflict",
"msg": "Replication conflict"
}
}
## ipahealthcheck.ipa.certs
### IPACertmongerExpirationCheck
Loops through all expected certmonger requests and checks expiration based on what certmonger knows about the certificate. A warning is issued if the certificate expires in cert_expiration_days (the default is 28).
Expired certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertmongerExpirationCheck",
"result": "ERROR",
"kw": {
"key": 1234,
"expiration_date", "20160101001704Z",
"msg": "Request id 1234 expired on 20160101001704Z"
}
}
Expiring certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertmongerExpirationCheck",
"result": "WARNING",
"kw": {
"key": 1234,
"expiration_date", "20160101001704Z",
"days": 9,
"msg": "Request id 1234 expires in 9 days"
}
}
### IPACertfileExpirationCheck
Similar to IPACertmongerExpirationCheck except the certificate is pulled from the PEM file or NSS database and re-verified. This is in case the certmonger tracking becomes out-of-sync with the certificate on disk.
The certificate file cannot be opened:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertfileExpirationCheck",
"result": "ERROR",
"kw": {
"key": 1234,
"certfile": "/path/to/cert.pem",
"error": [error],
"msg": "Unable to open cert file '/path/to/cert.pem': [error]"
}
}
The NSS database cannot be opened:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertfileExpirationCheck",
"result": "ERROR",
"kw": {
"key": 1234,
"dbdir": "/path/to/nssdb",
"error": [error],
"msg": "Unable to open NSS database '/path/to/nssdb': [error]"
}
}
The tracked nickname cannot be found in the NSS database:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertfileExpirationCheck",
"result": "ERROR",
"kw": {
"key": 1234,
"dbdir": "/path/to/nssdb",
"nickname": [nickname],
"error": [error],
"msg": "Unable to retrieve cert '[nickname]' from '/path/to/nssdb': [error]"
}
}
Expired certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertfileExpirationCheck",
"result": "ERROR",
"kw": {
"key": 1234,
"expiration_date", "20160101001704Z",
"msg": "Request id 1234 expired on 20160101001704Z"
}
}
Expiring certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertfileExpirationCheck",
"result": "WARNING",
"kw": {
"key": 1234,
"expiration_date", "20160101001704Z",
"days": 9,
"msg": "Request id 1234 expires in 9 days"
}
}
### IPACAChainExpirationCheck
Load the CA chain from /etc/ipa/ca.crt and test each one for expiration. This test is designed to ensure that the entire CA chain for all certificates is validated. For example, if the web or LDAP certificates have been replaced then the CA chain for those certs will reside in /etc/ipa/ca.crt. This includes an IPA CA signed by an external authority.
Expiring certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACAChainExpirationCheck",
"result": "WARNING",
"kw": {
"path": "/etc/ipa/ca.crt",
"key": "CN=Certificate Authority,O=EXAMPLE.TEST",
"days": 2,
"msg": "CA '{key}' is expiring in {days} days."
}
}
Expired certificate:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACAChainExpirationCheck",
"result": "CRITICAL",
"kw": {
"path": "/etc/ipa/ca.crt",
"key": "CN=Certificate Authority,O=EXAMPLE.TEST",
"msg": "CA '{key}' is expired."
}
}
### IPACertTracking
Compares the certmonger tracking on the system to the expected values. A query of the expected name/value pairs in certmonger is done to certmonger. On failure the contents of the query are missing. This result would be seen either if the certificate is tracked but there is some slight change in the expected value or if the tracking is missing entirely.
Missing certificate tracking:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertTracking",
"result": "ERROR",
"kw": {
"key": "cert-file=/var/lib/ipa/ra-agent.pem, key-file=/var/lib/ipa/ra-agent.key, ca-name=dogtag-ipa-ca-renew-agent, cert-storage=FILE, cert-presave-command=/usr/libexec/ipa/certmonger/renew_ra_cert_pre, cert-postsave-command=/usr/libexec/ipa/certmonger/renew_ra_cert"
"msg": "Missing tracking for cert-file=/var/lib/ipa/ra-agent.pem, key-file=/var/lib/ipa/ra-agent.key, ca-name=dogtag-ipa-ca-renew-agent, cert-storage=FILE, cert-presave-command=/usr/libexec/ipa/certmonger/renew_ra_cert_pre, cert-postsave-command=/usr/libexec/ipa/certmonger/renew_ra_cert"
}
}
An unknown certificate is being tracked by certmonger. This may be perfectly legitimate, it is provided for information only:
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertTracking",
"result": "WARNING",
"kw": {
"key": 1234,
"msg": "Unknown certmonger id 1234'
}
}
### IPACertNSSTrust
The trust for certificates stored in NSS databases is compared against a known good state.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertNSSTrust",
"result": "ERROR",
"kw": {
"key": "auditSigningCert cert-pki-ca",
"expected": "u,u,Pu",
"got": "u,u,u",
"nickname": "auditSigningCert cert-pki-ca",
"dbdir": "/etc/pki/pki-tomcat/alias",
"msg": "Incorrect NSS trust for auditSigningCert cert-pki-ca. Got u,u,u expected u,u,Pu"
}
}
### IPACertMatchCheck
Ensure CA certificate entries in LDAP and NSS databases match.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertMatchCheck",
"result": "ERROR",
"kw": {
"msg": "CA Certificate from /etc/ipa/nssdb does not match /etc/ipa/ca.crt"
}
}
### IPADogtagCertsMatchCheck
Check if Dogtag certificates present in both NSS DB and LDAP match.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPADogtagCertsMatchCheck",
"result": "ERROR",
"kw": {
"msg": "'subsystemCert cert-pki-ca' certificate in NSS DB does not match entry in LDAP"
}
}
### IPANSSChainValidation
Validate the certificate chain of the NSS certificates. This executes: certutil -V -u V -e -d [dbdir] -n [nickname].
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPANSSChainValidation",
"result": "ERROR",
"kw": {
"key": "/etc/dirsrv/slapd-EXAMPLE-TEST:Server-Cert",
"nickname": "Server-Cert",
"dbdir": [path to NSS database],
"reason": "certutil: certificate is invalid: Peer's Certificate issuer is not recognized.\n: ",
"msg": ""Validation of Server-Cert in /etc/dirsrv/slapd-EXAMPLE-TEST/ failed: certutil: certificate is invalid: Peer's Certificate issuer is not recognized.\n "
}
}
### IPAOpenSSLChainValidation
Validate the certificate chain of the OpenSSL certificates. This executes: openssl verify -verbose -show_chain -CAfile /etc/ipa/ca.crt /path/to/cert.pem
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPAOpenSSLChainValidation",
"result": "ERROR",
"kw": {
"key": "/var/lib/ipa/ra-agent.pem",
"reason": "O = EXAMPLE.TEST, CN = IPA RA\nerror 20 at 0 depth lookup: unable to get local issuer certificate\n",
"msg": "Certificate validation for /var/lib/ipa/ra-agent.pem failed: O = EXAMPLE.TEST, CN = IPA RA\nerror 20 at 0 depth lookup: unable to get local issuer certificate\n"
}
}
### IPARAAgent
Verify the description and userCertificate values in uid=ipara,ou=People,o=ipaca.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPARAAgent",
"result": "ERROR",
"kw": {
"expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST",
"got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST",
"msg": "RA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST"
}
}
### IPAKRAAgent
Verify the description and userCertificate values in uid=ipakra,ou=people,o=kra,o=ipaca.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPAKRAAgent",
"result": "ERROR",
"kw": {
"expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST",
"got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST",
"msg": "KRA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST"
}
}
### IPACertRevocation
Confirm that the IPA certificates are not revoked. This uses the certmonger tracking to determine the list of certificates to validate.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertRevocation",
"result": "ERROR",
"kw": {
"key": 1234,
"revocation_reason": "superseded",
"msg": "Certificate is revoked, superseded"
}
}
### IPACertmongerCA
Check that the certmonger CA configuration is correct. Evaluates dogtag-ipa-ca-renew-agent and dogtag-ipa-ca-renew-agent-reuse.
{
"source": "ipahealthcheck.ipa.certs",
"check": "IPACertmongerCA",
"result": "ERROR",
"kw": {
"key": "dogtag-ipa-ca-renew-agent",
"msg": "Certmonger CA 'dogtag-ipa-ca-renew-agent' missing"
}
}
## ipahealthcheck.ipa.dna
### IPADNARangeCheck
This reports the configured DNA range, if any. It is expected that this is combined elsewhere for further analysis.
{
"source": "ipahealthcheck.ipa.dna",
"check": "IPADNARangeCheck",
"result": "SUCCESS",
"kw": {
"range_start": 1000,
"range_max": 199999,
"next_start": 0,
"next_max": 0,
}
}
## ipahealthcheck.ipa.files
These checks verify the owner and mode of files installed or configured by IPA. There are many permutations of file permissions and ownership that may be valid and continue to work. This reports on the expected values in a fresh IPA installation. Deviations are reported at the WARNING level.
This covers the following checks:
### IPAFileNSSDBCheck
### IPAFileCheck
### TomcatFileCheck
Examples include:
{
"source": "ipahealthcheck.ipa.files",
"check": "IPAFileCheck",
"result": "WARNING",
"kw": {
"key": "_etc_ipa_ca.crt_mode",
"path": "/etc/ipa/ca.crt",
"type": "mode",
"expected": "0644",
"got": "0444",
"msg": "Permissions of /etc/ipa/ca.crt are 0444 and should be 0644"
}
}
{
"source": "ipahealthcheck.ipa.files",
"check": "IPAFileNSSDBCheck",
"result": "WARNING",
"kw": {
"key": "_etc_dirsrv_slapd-EXAMPLE-TEST_pkcs11.txt_mode",
"path": "/etc/dirsrv/slapd-EXAMPLE-TEST/pkcs11.txt",
"type": "mode",
"expected": "0640",
"got": "0666",
"msg": "Permissions of /etc/dirsrv/slapd-EXAMPLE-TEST/pkcs11.txt are 0666 and should be 0640"
}
},
## ipahealthcheck.ipa.host
### IPAHostKeytab
Executes: kinit -kt /etc/krb5.keytab to verify that the host keytab is valid.
## ipahealthcheck.ipa.roles
A set of information checks to report on whether the current master is the CRL generator and/or the renewal master.
### IPACRLManagerCheck
{
"source": "ipahealthcheck.ipa.roles",
"check": "IPACRLManagerCheck",
"result": "SUCCESS",
"kw": {
"key": "crl_manager",
"crlgen_enabled": true
}
},
### IPARenewalMasterCheck
{
"source": "ipahealthcheck.ipa.roles",
"check": "IPARenewalMasterCheck",
"result": "SUCCESS",
"kw": {
"key": "renewal_master",
"master": true
}
}
## ipahealthcheck.ipa.topology
Topology checks to check both for compliance with recommendations and errors.
### IPATopologyDomainCheck
Provide the equivalent of: ipa topologysuffix-verify
On failure this will return any errors discovered like connection errors or too many replication agreements.
On success it will return the configured domains.
{
"source": "ipahealthcheck.ipa.topology",
"check": "IPATopologyDomainCheck",
"result": "SUCCESS",
"kw": {
"suffix": "domain"
}
},
{
"source": "ipahealthcheck.ipa.topology",
"check": "IPATopologyDomainCheck",
"result": "SUCCESS",
"kw": {
"suffix": "ca"
}
}
## ipahealthcheck.ipa.trust
Verify common AD Trust configuration issues. Checks will return SUCCESS if not configured as a trust agent or controller.
### IPATrustAgentCheck
Check the sssd configuration when the machine is configured as a trust agent.
provider should be ipa and ipa_server_mode should be true.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustAgentCheck",
"severity": ERROR,
"kw": {
"key": "ipa_server_mode_false",
"attr": "ipa_server_mode",
"sssd_config": "/etc/sssd/sssd.conf",
"domain": "ipa.example.com",
"msg": "{attr} is not True in {sssd_config} in the domain {domain}"
}
}
### IPATrustDomainsCheck
Ensure that the IPA domain is in the output of sssctl domain-list and the trust domains matches the sssd domains.
If the domain lists don't match:
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustDomainsCheck",
"result": "ERROR",
"kw": {
"key": "domain-list",
"sslctl": "/usr/sbin/sssctl",
"sssd_domains": "ad.vm",
"trust_domains": "",
"msg": "{sslctl} {key} reports mismatch: sssd domains {sssd_domains} trust domains {trust_domains}"
}
}
### IPATrustCatalogCheck
This resolves an AD user, Administrator@REALM. This populates the AD Global catalog and AD Domain Controller values in sssctl domain-status output.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustCatalogCheck",
"result": "ERROR",
"kw": {
"key": "AD Global Catalog",
"output": "Active servers:\nAD Domain Controller: root-dc.ad.vm\nIPA: ipa.example.com",
"sssctl": "/usr/sbin/sssctl",
"domain": "ad.vm",
"msg": "{key} not found in {sssctl} 'domain-status' output: {output}"
}
}
### IPAsidgenpluginCheck
Verifies that the sidgen plugin is enabled in the IPA 389-ds instance.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPAsidgenpluginCheck",
"result": "ERROR",
"kw": {
"key": "IPA SIDGEN",
"error": "no such entry",
"msg": "Error retrieving 389-ds plugin {key}: {error}"
}
}
### IPATrustAgentMemberCheck
Verify that the current host is a member of cn=adtrust agents,cn=sysaccounts,cn=etc,SUFFIX.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustAgentMemberCheck",
"result": "ERROR",
"kw": {
"key": "ipa.example.com",
"group": "adtrust agents",
"msg": "{key} is not a member of {group}"
}
}
### IPATrustControllerPrincipalCheck
Verify that the current host cifs principal is a member of cn=adtrust agents,cn=sysaccounts,cn=etc,SUFFIX.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustControllerPrincipalCheck",
"result": "ERROR",
"kw": {
"key": "cifs/ipa.example.com@EXAMPLE.COM",
"group": "adtrust agents",
"msg": "{key} is not a member of {group}"
}
}
### IPATrustControllerServiceCheck
Verify that the current host starts the ADTRUST service in ipactl.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustControllerServiceCheck",
"result": "ERROR",
"kw": {
"key": "ADTRUST",
"msg": "{key} service is not enabled"
}
}
### IPATrustControllerConfCheck
Verify that ldapi is enabled for the passdb backend in the output of net conf list:
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustControllerConfCheck",
"result": "ERROR",
"kw": {
"key": "net conf list",
"got": "",
"expected": "ipasam:ldapi://%2fvar%2frun%2fslapd-EXAMPLE-COM.socket",
"option": "passdb backend",
"msg": "{key} option {option} value {got} doesn't match expected value {expected}"
}
}
### IPATrustControllerGroupSIDCheck
Verify that the admins group's SID ends with 512 (Domain Admins RID).
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustControllerGroupSIDCheck",
"result": "ERROR",
"kw": {
"key": "ipantsecurityidentifier",
"rid": "S-1-5-21-1078564529-1875285547-1976041503-513",
"msg": "{key} is not a Domain Admins RID"
}
}
### IPATrustPackageCheck
If not a trust controller and AD trust is enabled verify that the trust-ad pkg is installed.
{
"source": "ipahealthcheck.ipa.trust",
"check": "IPATrustPackageCheck",
"result": "WARNING",
"kw": {
"key": "adtrustpackage",
"msg": "trust-ad sub-package is not installed. Administration will be limited."
}
}
## ipahealthcheck.meta.services
Return the status of required IPA services
The following services are monitored:
* certmonger
* dirsrv
* gssproxy
* httpd
* ipa_custodia
* ipa_dnskeysyncd
* ipa_otpd
* kadmin
* krb5kdc
* named
* pki_tomcatd
* sssd
The value of check is the name of the IPA service. Note that dashes are replaced with underscores in the service names.
An example of a stopped service:
{
"source": "ipahealthcheck.meta.services",
"check": "httpd",
"result": "ERROR",
"kw": {
"status": false,
"msg": "httpd: not running"
}
}
## ipahealthcheck.meta.core
Provide basic information about the IPA master itself.
### MetaCheck
Output includes the FQDN and the version of IPA.
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "ipa.example.test",
"ipa_version": "4.8.0",
"ipa_api_version": "2.233"
}
}
## ipahealthcheck.system.filesystemspace
Check on available disk space. Running low can cause issues with logging, execution and backups.
### FileSystemSpaceCheck
Both a percentage and raw minimum values are tested.
It is possible there is some overlap depending on mount points.
The minimum free is 20 percent and is currently hard coded.
The following paths are checked:
Path free MB
/var/lib/dirsrv/ 1024
/var/lib/ipa/backup/ 512
/var/log/ 1024
/var/log/audit/ 512
/var/tmp/ 512
/tmp 512
For example a full /tmp would be reported as:
{
"source": "ipahealthcheck.system.filesystemspace",
"check": "FileSystemSpaceCheck",
"result": "ERROR",
"kw": {
"msg": "/tmp: free space percentage under threshold: 0% < 20%",
"store": "/tmp",
"percent_free": 0,
"threshold": 20
}
},
{
"source": "ipahealthcheck.system.filesystemspace",
"check": "FileSystemSpaceCheck",
"result": "ERROR",
"kw": {
"msg": "/tmp: free space under threshold: 0 MiB < 512 MiB",
"store": "/tmp",
"free_space": 0,
"threshold": 512
}
}
freeipa-healthcheck-0.10/logrotate/ 0000775 0000000 0000000 00000000000 14200534377 0017313 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/logrotate/ipahealthcheck 0000664 0000000 0000000 00000000152 14200534377 0022171 0 ustar 00root root 0000000 0000000 /var/log/ipa/healthcheck/healthcheck.log {
create
daily
dateext
missingok
rotate 30
}
freeipa-healthcheck-0.10/man/ 0000775 0000000 0000000 00000000000 14200534377 0016066 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/man/man5/ 0000775 0000000 0000000 00000000000 14200534377 0016726 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/man/man5/ipahealthcheck.conf.5 0000664 0000000 0000000 00000010512 14200534377 0022674 0 ustar 00root root 0000000 0000000 .\" A man page for ipahealthcheck.conf
.\" Copyright (C) 2019 FreeIPA Contributors see COPYING for license
.\"
.TH "ipahealthcheckconf" "5" "Apr 5 2019" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipahealthcheck.conf \- ipa-healthcheck configuration file
.SH "SYNOPSIS"
/etc/ipahealthcheck/ipahealthcheck.conf
.SH "DESCRIPTION"
The \fIipahealthcheck.conf \fRconfiguration file is used to set the defaults when running ipa\-healthcheck.
.SH "SYNTAX"
The configuration options are not case sensitive. The values may be case sensitive, depending on the option.
Blank lines are ignored.
Lines beginning with # are comments and are ignored.
Valid lines consist of an option name, an equals sign and a value. Spaces surrounding equals sign are ignored. An option terminates at the end of a line.
Values should not be quoted, the quotes will not be stripped.
.DS L
# Wrong \- don't include quotes
verbose = "True"
# Right \- Properly formatted options
verbose = True
verbose=True
.DE
Options must appear in the section named [default]. There are no other sections defined or used currently.
Options may be defined that are not used. Be careful of misspellings, they will not be rejected.
.SH "EXCLUDES"
There may be reasons that a user will want to suppress some results. One example is a customer certificate that is generating a warning because it is unknown to IPA. Excluding a result key does not prevent it from running, it is filtered from the reported results. Excluding by source or key will prevent it from running at all. Services will not be excluded because other checks may rely on them (ipahealthcheck.meta.services).
Each excludes type may be listed multiple times. Invalid sources, checks and/or keys will not be flagged. These configuration options are only processed when found in the EXCLUDES section and are otherwise ignored.
Users are encouraged to annotate the reason for excluding the results so the reason is not lost.
Results can be suppressed or excluded in three ways:
.IP
\(bu source, e.g. ipahealthcheck.ipa.certs
.IP
\(bu check, e.g. IPADNSSystemRecordsCheck
.IP
\(bu report key, e.g. 20210910141457 (certmonger tracking id)
.SH "OPTIONS"
The following options are relevant in each section.
.TP
[default]
.TP
.B cert_expiration_days\fR
The number of days left before a certificate expires to start displaying a warning. The default is 28.
.TP
.B timeout\fR
The time allowed in seconds for each check to run before being considered an error. The default is 10.
.TP
[excludes]
.TP
.B source\fR
Filter results based on the check source.
.TP
.B check\fR
Filter results based on the check name.
.TP
.B key\fR
Filter results based on the result key in the healthcheck output.
.TP
All command\-line options may be included in the configuration file. Dashes must be converted to underscore for the configuration file, e.g. \-\-output\-type becomes output_type. All options, including those that don't make sense in a config file, like \-\-list\-sources, are allowed. Let the buyer beware.
.TP
The purpose of allowing command\-line options to be in the configuration file is for automation without having to tweak the automation script. For example, if you want the default output type to be human for the systemd timer automated runs, settting output_type=human in the configuration file will do this. When loading configuration the first option wins, so if any option is in the configuration file then it cannot be overridden by the command-line unless a different configuration file is specified (see \-\-config).
.TP
There may be conflicting exceptions. For example, if all=True is set in the configuration file, and the command\-line contains \-\-failures\-only, then only failures will be displayed because of the way the option evaluation is done.
.TP
Options that don't make sense for the configuration file include \-\-list\-sources and \-\-input\-file.
.TP
.SH "FILES"
.TP
.I /etc/ipahealthcheck/ipahealthcheck.conf
configuration file
.SH "EXAMPLES"
.TP
7 days left before a certificate expires to start displaying a warning:
[default]
cert_expiration_days=7
timeout=5
.RS L
[default]
cert_expiration_days=7
.RE
Exclude all certificate checks.
.RS L
[excludes]
source=ipahealthcheck.ipa.certs
.RE
Don't warn about a custom certificate being tracked by certmonger:
.RS L
[excludes]
key=20210910141452
.RE
.SH "SEE ALSO"
.BR ipa\-healthcheck (8)
freeipa-healthcheck-0.10/man/man8/ 0000775 0000000 0000000 00000000000 14200534377 0016731 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/man/man8/ipa-healthcheck.8 0000664 0000000 0000000 00000011453 14200534377 0022040 0 ustar 00root root 0000000 0000000 .\" A man page for ipa-healthcheck
.\" Copyright (C) 2019 FreeIPA Contributors see COPYING for license
.\"
.TH "ipa-healthcheck" "8" "Jan 16 2020" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipa\-healthcheck \- Check on the health of an IPA installation
.SH "SYNOPSIS"
ipa\-healthcheck [\fIOPTION\fR]...
.SH "DESCRIPTION"
An IPA installation is a complex system and identifying real or potential issues can be difficult and require a lot of analysis. This tool aims to reduce the burden of that and attempts to identify issues in advance so they can be corrected, ideally before the issue is critical.
.SS "ORGANIZATION"
These areas of the system to check can be logically grouped together. This grouping is called a source. A source consists of one or more checks.
A check is as atomic as possible to limit the scope and complexity and provide a yes/no answer on whether that particular configuration is correct.
Each check will return a result, either a result of WARNING, ERROR or CRITICAL or SUCCESS. Returning SUCCESS tells you that the check was done and was deemed correct. This should help track when the last time something was examined.
Upon failure the output will include the source and check that detected the failure along with a message and name/value pairs indicating the problem. It may very well be that the check can't make a final determination and generally defaults to WARNING if it can't be sure so that it can be examined.
.SS "IMPLEMENTATION DETAILS"
There is no need for users to authenticate and get a ticket in advance for ipa\-healthcheck to work. Existing tickets will not be used as ipa\-healthcheck will leverage the host keytab and use a temporary credential cache.
.SH "OPTIONS"
.SS "COMMANDS"
.TP
\fB\-\-list\-sources\fR
Display a list of the available sources and the checks associated with those sources.
.SS "OPTIONAL ARGUMENTS"
.TP
\fB\-\-config\fR=\fIFILE\fR
The configuration file to use. If an empty string is passed in then no configuration file is loaded. The default is /etc/ipahealthcheck/ipahealthcheck.conf.
.TP
\fB\-\-source\fR=\fISOURCE\fR
Execute checks within the named source, or all sources in the given namespace.
.TP
\fB\-\-check\fR=\fICHECK\fR
Execute this particular check within a source. The exact source must also be specified via \fB\-\-source\fR.
.TP
\fB\-\-output\-type\fR=\fITYPE\fR
Set the output type. Supported variants are \fBhuman\fR, \fBjson\fR, and \fBprometheus\fR. The default is \fBjson\fR.
.TP
\fB\-\-failures\-only\fR
Exclude SUCCESS results on output. If stdin is a tty then this will default to True. In all other cases it defaults to False.
.TP
\fB\-\-all\fR
Report all results.
.TP
\fB\-\-severity=\fRSEVERITY\fR
Only report errors in the requested severity of SUCCESS, WARNING, ERROR or CRITICAL. This can be provided multiple times to search on multiple levels.
.TP
\fB\-\-verbose\fR
Generate verbose output.
.TP
\fB\-\-debug\fR
Generate additional debugging output.
.SS "JSON OUTPUT"
The results are displayed as a list of result messages for each check executed in JSON format. This could be input for a monitoring system.
.TP
\fB\-\-output\-file\fR=\fIFILENAME\fR
Write the output to this filename rather than stdout.
.TP
\fB\-\-input\-file\fR=\fIFILENAME\fR
Read the results of a previous run and re-display them.
.TP
\fB\-\-indent\fR=\fIINDENT\fR
Pretty\-print the JSON with this indention level. This can make the output more human\-readable.
.SS "HUMAN\-READABLE OUTPUT"
The results are displayed in a more human\-readable format.
.TP
\fB\-\-input\-file\fR=\fIFILENAME\fR
Take as input a JSON results output and convert it to a more human\-readable form.
.SS "PROMETHEUS OUTPUT"
The results are displayed in the Prometheus text metric exposition format.
.TP
\fB\-\-input\-file\fR=\fIFILENAME\fR
Uses the JSON-formatted results output as metrics source.
\fB\-\-metric\-prefix\fR=\fIPREFIX\fR
Prefix to use for metric names.
.SH "EXAMPLES"
.PP
Execute healthcheck with the default JSON output:
.PP
.nf 1
\ ipa\-healthcheck
.fi
.PP
Execute healthcheck with a prettier JSON output:
.PP
.nf 1
\ ipa\-healthcheck \-\-indent 2
.fi
.PP
Execute healthcheck and only display errors:
.PP
.nf 1
\ ipa\-healthcheck \-\-failures\-only
.fi
.PP
Display in human\-readable output a previous report:
.PP
.nf 2
\ ipa\-healthcheck \-\-output\-type human \-\-input\-file \e
\& /var/log/ipa/healthcheck/healthcheck.log
.fi
.SH "FILES"
.TP
/etc/ipahealthcheck/ipahealthcheck.conf
.SH "EXIT STATUS"
0 if all checks were successful
1 if any one check failed or the command failed to execute properly
.SH INTERNET RESOURCES
Main website: https://www.freeipa.org/
Git repository for ipa-healthcheck: https://www.github.com/freeipa/freeipa-healthcheck/
.SH OTHER RESOURCES
The ipa-healthcheck distribution includes a documentation file named README.md which contains detailed explanations on executed checks.
freeipa-healthcheck-0.10/pylint_plugins.py 0000664 0000000 0000000 00000041166 14200534377 0020755 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
#
from __future__ import print_function
import copy
import os.path
import sys
import textwrap
from astroid import MANAGER, register_module_extender
from astroid import scoped_nodes
from pylint.checkers import BaseChecker
from pylint.checkers.utils import check_messages
from pylint.interfaces import IAstroidChecker
from astroid.builder import AstroidBuilder
def register(linter):
linter.register_checker(IPAChecker(linter))
def _warning_already_exists(cls, member):
print(
"WARNING: member '{member}' in '{cls}' already exists".format(
cls="{}.{}".format(cls.root().name, cls.name), member=member),
file=sys.stderr
)
def fake_class(name_or_class_obj, members=()):
if isinstance(name_or_class_obj, scoped_nodes.ClassDef):
cl = name_or_class_obj
else:
cl = scoped_nodes.ClassDef(name_or_class_obj, None)
for m in members:
if isinstance(m, str):
if m in cl.locals:
_warning_already_exists(cl, m)
else:
cl.locals[m] = [scoped_nodes.ClassDef(m, None)]
elif isinstance(m, dict):
for key, val in m.items():
assert isinstance(key, str), "key must be string"
if key in cl.locals:
_warning_already_exists(cl, key)
fake_class(cl.locals[key], val)
else:
cl.locals[key] = [fake_class(key, val)]
else:
# here can be used any astroid type
if m.name in cl.locals:
_warning_already_exists(cl, m.name)
else:
cl.locals[m.name] = [copy.copy(m)]
return cl
# 'class': ['generated', 'properties']
ipa_class_members = {
# Python standard library & 3rd party classes
'socket._socketobject': ['sendall'],
# IPA classes
'ipalib.base.NameSpace': [
'add',
'mod',
'del',
'show',
'find'
],
'ipalib.cli.Collector': ['__options'],
'ipalib.config.Env': [ # somehow needed for pylint on Python 2
'debug',
'startup_traceback',
'server',
'validate_api',
'verbose',
],
'ipalib.errors.ACIError': [
'info',
],
'ipalib.errors.ConversionError': [
'error',
],
'ipalib.errors.DatabaseError': [
'desc',
],
'ipalib.errors.NetworkError': [
'error',
],
'ipalib.errors.NotFound': [
'reason',
],
'ipalib.errors.PublicError': [
'msg',
'strerror',
'kw',
],
'ipalib.errors.SingleMatchExpected': [
'found',
],
'ipalib.errors.SkipPluginModule': [
'reason',
],
'ipalib.errors.ValidationError': [
'error',
],
'ipalib.errors.SchemaUpToDate': [
'fingerprint',
'ttl',
],
'ipalib.messages.PublicMessage': [
'msg',
'strerror',
'type',
'kw',
],
'ipalib.parameters.Param': [
'cli_name',
'cli_short_name',
'label',
'default',
'doc',
'required',
'multivalue',
'primary_key',
'normalizer',
'default_from',
'autofill',
'query',
'attribute',
'include',
'exclude',
'flags',
'hint',
'alwaysask',
'sortorder',
'option_group',
'no_convert',
'deprecated',
],
'ipalib.parameters.Bool': [
'truths',
'falsehoods'],
'ipalib.parameters.Data': [
'minlength',
'maxlength',
'length',
'pattern',
'pattern_errmsg',
],
'ipalib.parameters.Str': ['noextrawhitespace'],
'ipalib.parameters.Password': ['confirm'],
'ipalib.parameters.File': ['stdin_if_missing'],
'ipalib.parameters.Enum': ['values'],
'ipalib.parameters.Number': [
'minvalue',
'maxvalue',
],
'ipalib.parameters.Decimal': [
'precision',
'exponential',
'numberclass',
],
'ipalib.parameters.DNSNameParam': [
'only_absolute',
'only_relative',
],
'ipalib.parameters.Principal': [
'require_service',
],
'ipalib.plugable.API': [
'Advice',
],
'ipalib.util.ForwarderValidationError': [
'msg',
],
'ipaserver.plugins.dns.DNSRecord': [
'validatedns',
'normalizedns',
],
}
def fix_ipa_classes(cls):
class_name_with_module = "{}.{}".format(cls.root().name, cls.name)
if class_name_with_module in ipa_class_members:
fake_class(cls, ipa_class_members[class_name_with_module])
MANAGER.register_transform(scoped_nodes.ClassDef, fix_ipa_classes)
def ipaplatform_constants_transform():
return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
from ipaplatform.base.constants import constants, User, Group
__all__ = ('constants', 'User', 'Group')
'''))
def ipaplatform_paths_transform():
return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
from ipaplatform.base.paths import paths
__all__ = ('paths',)
'''))
def ipaplatform_services_transform():
return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
from ipaplatform.base.services import knownservices
from ipaplatform.base.services import timedate_services
from ipaplatform.base.services import service
from ipaplatform.base.services import wellknownservices
from ipaplatform.base.services import wellknownports
__all__ = ('knownservices', 'timedate_services', 'service',
'wellknownservices', 'wellknownports')
'''))
def ipaplatform_tasks_transform():
return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
from ipaplatform.base.tasks import tasks
__all__ = ('tasks',)
'''))
register_module_extender(MANAGER, 'ipaplatform.constants',
ipaplatform_constants_transform)
register_module_extender(MANAGER, 'ipaplatform.paths',
ipaplatform_paths_transform)
register_module_extender(MANAGER, 'ipaplatform.services',
ipaplatform_services_transform)
register_module_extender(MANAGER, 'ipaplatform.tasks',
ipaplatform_tasks_transform)
def ipalib_request_transform():
"""ipalib.request.context attribute
"""
return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
from ipalib.request import context
context._pylint_attr = Connection("_pylint", lambda: None)
'''))
register_module_extender(MANAGER, 'ipalib.request', ipalib_request_transform)
class IPAChecker(BaseChecker):
__implements__ = IAstroidChecker
name = 'ipa'
msgs = {
'W9901': (
'Forbidden import %s (can\'t import from %s in %s)',
'ipa-forbidden-import',
'Used when an forbidden import is detected.',
),
}
options = (
(
'forbidden-imports',
{
'default': '',
'type': 'csv',
'metavar': '[:[:...]][,...]',
'help': 'Modules which are forbidden to be imported in the '
'given paths',
},
),
)
priority = -1
def open(self):
self._dir = os.path.abspath(os.path.dirname(__file__))
self._forbidden_imports = {self._dir: []}
for forbidden_import in self.config.forbidden_imports:
forbidden_import = forbidden_import.split(':')
path = os.path.join(self._dir, forbidden_import[0])
path = os.path.abspath(path)
modules = forbidden_import[1:]
self._forbidden_imports[path] = modules
self._forbidden_imports_stack = []
def _get_forbidden_import_rule(self, node):
path = node.path
if path and isinstance(path, list):
# In pylint 2.0, path is a list with one element. Namespace
# packages may contain more than one element, but we can safely
# ignore them, as they don't contain code.
path = path[0]
if path:
path = os.path.abspath(path)
while path.startswith(self._dir):
if path in self._forbidden_imports:
return path
path = os.path.dirname(path)
return self._dir
def visit_module(self, node):
self._forbidden_imports_stack.append(
self._get_forbidden_import_rule(node))
def leave_module(self, node):
self._forbidden_imports_stack.pop()
def _check_forbidden_imports(self, node, names):
path = self._forbidden_imports_stack[-1]
relpath = os.path.relpath(path, self._dir)
modules = self._forbidden_imports[path]
for module in modules:
module_prefix = module + '.'
for name in names:
if name == module or name.startswith(module_prefix):
self.add_message('ipa-forbidden-import',
args=(name, module, relpath), node=node)
@check_messages('ipa-forbidden-import')
def visit_import(self, node):
names = [n[0] for n in node.names]
self._check_forbidden_imports(node, names)
@check_messages('ipa-forbidden-import')
def visit_importfrom(self, node):
names = ['{}.{}'.format(node.modname, n[0]) for n in node.names]
self._check_forbidden_imports(node, names)
#
# Teach pylint how api object works
#
# ipalib uses some tricks to create api.env members and api objects. pylint
# is not able to infer member names and types from code. The explict
# assignments inside the string builder templates are good enough to show
# pylint, how the api is created. Additional transformations are not
# required.
#
AstroidBuilder(MANAGER).string_build(textwrap.dedent(
"""
from ipalib import api
from ipalib import cli, plugable, rpc
from ipalib.base import NameSpace
from ipaclient.plugins import rpcclient
try:
from ipaserver.plugins import dogtag, ldap2, serverroles
except ImportError:
HAS_SERVER = False
else:
HAS_SERVER = True
def wildcard(*args, **kwargs):
return None
# ipalib.api members
api.Backend = plugable.APINameSpace(api, None)
api.Command = plugable.APINameSpace(api, None)
api.Method = plugable.APINameSpace(api, None)
api.Object = plugable.APINameSpace(api, None)
api.Updater = plugable.APINameSpace(api, None)
# ipalib.api.Backend members
api.Backend.cli = cli.cli(api)
api.Backend.textui = cli.textui(api)
api.Backend.jsonclient = rpc.jsonclient(api)
api.Backend.rpcclient = rpcclient.rpcclient(api)
api.Backend.xmlclient = rpc.xmlclient(api)
if HAS_SERVER:
api.Backend.kra = dogtag.kra(api)
api.Backend.ldap2 = ldap2.ldap2(api)
api.Backend.ra = dogtag.ra(api)
api.Backend.ra_certprofile = dogtag.ra_certprofile(api)
api.Backend.ra_lightweight_ca = dogtag.ra_lightweight_ca(api)
api.Backend.serverroles = serverroles.serverroles(api)
# ipalib.base.NameSpace
NameSpace.find = wildcard
"""
))
AstroidBuilder(MANAGER).string_build(textwrap.dedent(
"""
from ipalib import api
from ipapython.dn import DN
api.env.api_version = ''
api.env.bin = '' # object
api.env.ca_agent_port = 0
api.env.ca_host = ''
api.env.ca_install_port = None
api.env.ca_port = 0
api.env.certmonger_wait_timeout = 0
api.env.conf = '' # object
api.env.conf_default = '' # object
api.env.confdir = '' # object
api.env.container_accounts = DN()
api.env.container_adtrusts = DN()
api.env.container_applications = DN()
api.env.container_automember = DN()
api.env.container_automount = DN()
api.env.container_ca = DN()
api.env.container_ca_renewal = DN()
api.env.container_caacl = DN()
api.env.container_certmap = DN()
api.env.container_certmaprules = DN()
api.env.container_certprofile = DN()
api.env.container_cifsdomains = DN()
api.env.container_configs = DN()
api.env.container_custodia = DN()
api.env.container_deleteuser = DN()
api.env.container_dna = DN()
api.env.container_dna_posix_ids = DN()
api.env.container_dns = DN()
api.env.container_dnsservers = DN()
api.env.container_group = DN()
api.env.container_hbac = DN()
api.env.container_hbacservice = DN()
api.env.container_hbacservicegroup = DN()
api.env.container_host = DN()
api.env.container_hostgroup = DN()
api.env.container_locations = DN()
api.env.container_masters = DN()
api.env.container_netgroup = DN()
api.env.container_otp = DN()
api.env.container_permission = DN()
api.env.container_policies = DN()
api.env.container_policygroups = DN()
api.env.container_policylinks = DN()
api.env.container_privilege = DN()
api.env.container_radiusproxy = DN()
api.env.container_ranges = DN()
api.env.container_realm_domains = DN()
api.env.container_rolegroup = DN()
api.env.container_roles = DN()
api.env.container_s4u2proxy = DN()
api.env.container_selinux = DN()
api.env.container_service = DN()
api.env.container_stageuser = DN()
api.env.container_sudocmd = DN()
api.env.container_sudocmdgroup = DN()
api.env.container_sudorule = DN()
api.env.container_sysaccounts = DN()
api.env.container_topology = DN()
api.env.container_trusts = DN()
api.env.container_user = DN()
api.env.container_vault = DN()
api.env.container_views = DN()
api.env.container_virtual = DN()
api.env.context = '' # object
api.env.debug = False
api.env.delegate = False
api.env.dogtag_version = 0
api.env.dot_ipa = '' # object
api.env.enable_ra = False
api.env.env_confdir = None
api.env.fallback = True
api.env.force_schema_check = False
api.env.home = '' # object
api.env.host = ''
api.env.host_princ = ''
api.env.http_timeout = 0
api.env.in_server = False # object
api.env.in_tree = False # object
api.env.interactive = True
api.env.ipalib = '' # object
api.env.kinit_lifetime = None
api.env.lite_pem = ''
api.env.lite_profiler = ''
api.env.lite_host = ''
api.env.lite_port = 0
api.env.log = '' # object
api.env.logdir = '' # object
api.env.mode = ''
api.env.mount_ipa = ''
api.env.nss_dir = '' # object
api.env.plugins_on_demand = False # object
api.env.prompt_all = False
api.env.ra_plugin = ''
api.env.recommended_max_agmts = 0
api.env.replication_wait_timeout = 0
api.env.rpc_protocol = ''
api.env.server = ''
api.env.script = '' # object
api.env.site_packages = '' # object
api.env.skip_version_check = False
api.env.smb_princ = ''
api.env.startup_timeout = 0
api.env.startup_traceback = False
api.env.tls_ca_cert = '' # object
api.env.tls_version_max = ''
api.env.tls_version_min = ''
api.env.validate_api = False
api.env.verbose = 0
api.env.version = ''
api.env.wait_for_dns = 0
api.env.webui_prod = True
"""
))
# dnspython 2.x introduces enums and creates module level globals from them
# pylint does not understand the trick
AstroidBuilder(MANAGER).string_build(textwrap.dedent(
"""
import dns.flags
import dns.rdataclass
import dns.rdatatype
dns.flags.AD = 0
dns.flags.CD = 0
dns.flags.DO = 0
dns.flags.RD = 0
dns.rdataclass.IN = 0
dns.rdatatype.A = 0
dns.rdatatype.AAAA = 0
dns.rdatatype.CNAME = 0
dns.rdatatype.DNSKEY = 0
dns.rdatatype.MX = 0
dns.rdatatype.NS = 0
dns.rdatatype.PTR = 0
dns.rdatatype.RRSIG = 0
dns.rdatatype.SOA = 0
dns.rdatatype.SRV = 0
dns.rdatatype.TXT = 0
dns.rdatatype.URI = 0
"""
))
AstroidBuilder(MANAGER).string_build(
textwrap.dedent(
"""\
from ipatests.test_integration.base import IntegrationTest
from ipatests.pytest_ipa.integration.host import Host, WinHost
from ipatests.pytest_ipa.integration.config import Config, Domain
class PylintIPAHosts:
def __getitem__(self, key):
return Host()
class PylintWinHosts:
def __getitem__(self, key):
return WinHost()
class PylintADDomains:
def __getitem__(self, key):
return Domain()
Host.config = Config()
Host.domain = Domain()
IntegrationTest.domain = Domain()
IntegrationTest.master = Host()
IntegrationTest.replicas = PylintIPAHosts()
IntegrationTest.clients = PylintIPAHosts()
IntegrationTest.ads = PylintWinHosts()
IntegrationTest.ad_treedomains = PylintWinHosts()
IntegrationTest.ad_subdomains = PylintWinHosts()
IntegrationTest.ad_domains = PylintADDomains()
"""
)
)
freeipa-healthcheck-0.10/pylintrc 0000664 0000000 0000000 00000010273 14200534377 0017105 0 ustar 00root root 0000000 0000000 [MASTER]
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
# FIXME: has to be specified on the command line otherwise pylint fails with
# DuplicateSectionError for the IPA section
#load-plugins=pylint_plugins
# Use multiple processes to speed up Pylint.
jobs=0
# A list of packages with safe C extensions to load
extension-pkg-whitelist=
_ldap,
cryptography,
gssapi,
netifaces
[CLASSES]
# List of valid names for the first argument in a metaclass class method.
# This can be removed after upgrading to pylint 2.0
valid-metaclass-classmethod-first-arg=cls
[MESSAGES CONTROL]
enable=
all,
python3
disable=
I,
duplicate-code,
interface-not-implemented,
no-self-use,
redefined-variable-type,
too-few-public-methods,
too-many-ancestors,
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
abstract-method,
anomalous-backslash-in-string,
arguments-differ,
attribute-defined-outside-init,
bad-builtin,
bad-indentation,
broad-except,
consider-using-dict-items,
dangerous-default-value,
eval-used,
exec-used,
fixme,
global-statement,
no-init,
pointless-string-statement,
protected-access,
redefined-builtin,
redefined-outer-name,
super-init-not-called,
undefined-loop-variable,
unnecessary-lambda,
unused-argument,
useless-else-on-loop,
bad-continuation,
bad-whitespace,
blacklisted-name,
invalid-name,
line-too-long,
missing-docstring,
multiple-statements,
superfluous-parens,
too-many-lines,
unidiomatic-typecheck,
no-absolute-import,
wrong-import-order,
ungrouped-imports,
wrong-import-position,
unsubscriptable-object,
unsupported-membership-test,
not-an-iterable,
singleton-comparison,
misplaced-comparison-constant,
not-a-mapping,
singleton-comparison,
len-as-condition, # new in pylint 1.7
no-else-return, # new in pylint 1.7
single-string-used-for-slots, # new in pylint 1.7
useless-super-delegation, # new in pylint 1.7
redefined-argument-from-local, # new in pylint 1.7
consider-merging-isinstance, # new in pylint 1.7
bad-option-value, # required to support upgrade to pylint 2.0
assignment-from-no-return, # new in pylint 2.0
keyword-arg-before-vararg, # pylint 2.0, remove after dropping Python 2
consider-using-enumerate, # pylint 2.1, clean up tests later
no-else-raise, # python 2.4.0
import-outside-toplevel, # pylint 2.4.2
f-string-without-interpolation, # pylint 2.5.0, bare f-strings are ok
super-with-arguments, # pylint 2.6.0, zero-length form is syntactic sugar
raise-missing-from, # pylint 2.6.0, implicit exception chaining is ok
consider-using-with, # pylint 2.8.0, contextmanager is not mandatory
consider-using-max-builtin, # pylint 2.8.0, can be more readable
consider-using-min-builtin, # pylint 2.8.0, can be more readable
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=colorized
# Tells whether to display a full report or only the messages
reports=no
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
msg-template='{path}:{line}: [{msg_id}({symbol}), {obj}] {msg})'
[VARIABLES]
dummy-variables-rgx=(_.+|unused)
[IPA]
forbidden-imports=
client/:ipaserver,
ipaclient/:ipaclient.install:ipalib.install:ipaserver,
ipaclient/install/:ipaserver,
ipalib/:ipaclient.install:ipalib.install:ipaserver,
ipalib/install/:ipaserver,
ipaplatform/:ipaclient:ipalib:ipaserver,
ipapython/:ipaclient:ipalib:ipaserver
ipatests/pytest_ipa:ipaserver:ipaclient.install:ipalib.install
ipatests/test_integration:ipaserver
freeipa-healthcheck-0.10/setup.cfg 0000664 0000000 0000000 00000000026 14200534377 0017132 0 ustar 00root root 0000000 0000000 [aliases]
test=pytest
freeipa-healthcheck-0.10/setup.py 0000664 0000000 0000000 00000006522 14200534377 0017032 0 ustar 00root root 0000000 0000000 from setuptools import find_packages, setup
setup(
name='ipahealthcheck',
version='0.10',
namespace_packages=['ipahealthcheck', 'ipaclustercheck'],
package_dir={'': 'src'},
# packages=find_packages(where='src'),
packages=[
'ipahealthcheck.core',
'ipahealthcheck.dogtag',
'ipahealthcheck.ds',
'ipahealthcheck.ipa',
'ipahealthcheck.meta',
'ipahealthcheck.system',
'ipaclustercheck.core',
'ipaclustercheck.ipa',
],
entry_points={
# creates bin/ipahealthcheck
'console_scripts': [
'ipa-healthcheck = ipahealthcheck.core.main:main',
'ipa-clustercheck = ipaclustercheck.core.main:main',
],
# subsystem registries
'ipahealthcheck.registry': [
'ipahealthcheck.dogtag = ipahealthcheck.dogtag.plugin:registry',
'ipahealthcheck.ipa = ipahealthcheck.ipa.plugin:registry',
'ipahealthcheck.meta = ipahealthcheck.meta.plugin:registry',
'ipahealthcheck.ds = ipahealthcheck.ds.plugin:registry',
'ipahealthcheck.system = ipahealthcheck.system.plugin:registry'
],
# plugin modules for ipahealthcheck.meta registry
'ipahealthcheck.meta': [
'meta = ipahealthcheck.meta.core',
'services = ipahealthcheck.meta.services',
],
# plugin modules for ipahealthcheck.ipa registry
'ipahealthcheck.ipa': [
'ipacerts = ipahealthcheck.ipa.certs',
'ipadna = ipahealthcheck.ipa.dna',
'ipadns = ipahealthcheck.ipa.idns',
'ipafiles = ipahealthcheck.ipa.files',
'ipahost = ipahealthcheck.ipa.host',
'ipaproxy = ipahealthcheck.ipa.proxy',
'ipameta = ipahealthcheck.ipa.meta',
'ipanss = ipahealthcheck.ipa.nss',
'iparoles = ipahealthcheck.ipa.roles',
'ipatopology = ipahealthcheck.ipa.topology',
'ipatrust = ipahealthcheck.ipa.trust',
],
# plugin modules for ipahealthcheck.dogtag registry
'ipahealthcheck.dogtag': [
'dogtagca = ipahealthcheck.dogtag.ca',
],
# plugin modules for ipahealthcheck.ds registry
'ipahealthcheck.ds': [
'dsbackends = ipahealthcheck.ds.backends',
'dsconfig = ipahealthcheck.ds.config',
'dsdiskspace = ipahealthcheck.ds.disk_space',
'dsdse = ipahealthcheck.ds.dse',
'dsencryption = ipahealthcheck.ds.encryption',
'dsfschecks = ipahealthcheck.ds.fs_checks',
'dsnssssl = ipahealthcheck.ds.nss_ssl',
'dsplugins = ipahealthcheck.ds.ds_plugins',
'dsreplication = ipahealthcheck.ds.replication',
'dsruv = ipahealthcheck.ds.ruv',
],
# plugin modules for ipahealthcheck.system registry
'ipahealthcheck.system': [
'filesystemspace = ipahealthcheck.system.filesystemspace',
],
'ipaclustercheck.registry': [
'ipaclustercheck.ipa = ipaclustercheck.ipa.plugin:registry',
],
'ipaclustercheck.ipa': [
'crl = ipaclustercheck.ipa.crlmanager',
'ruv = ipaclustercheck.ipa.ruv',
],
},
classifiers=[
'Programming Language :: Python :: 3.6',
],
python_requires='!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
)
freeipa-healthcheck-0.10/src/ 0000775 0000000 0000000 00000000000 14200534377 0016102 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/ 0000775 0000000 0000000 00000000000 14200534377 0021253 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/__init__.py 0000664 0000000 0000000 00000000177 14200534377 0023371 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
__import__('pkg_resources').declare_namespace(__name__)
freeipa-healthcheck-0.10/src/ipaclustercheck/core/ 0000775 0000000 0000000 00000000000 14200534377 0022203 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/core/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024302 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/core/main.py 0000664 0000000 0000000 00000001551 14200534377 0023503 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
import sys
from ipaclustercheck.core.output import output_registry
from ipahealthcheck.core.core import RunChecks
class ClusterChecks(RunChecks):
def add_options(self):
parser = self.parser
parser.add_argument('--directory', dest='dir',
help='Directory holding healthcheck logs')
def validate_options(self):
super().validate_options()
if self.options.dir is None:
print("--directory containing logs to check is required")
return 1
return None
def main():
clusterchecks = ClusterChecks(['ipaclustercheck.registry'],
'/etc/ipa/clustercheck.conf',
output_registry, 'ansible')
sys.exit(clusterchecks.run_healthcheck())
freeipa-healthcheck-0.10/src/ipaclustercheck/core/output.py 0000664 0000000 0000000 00000003320 14200534377 0024113 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import json
from ipahealthcheck.core.output import OutputRegistry, Output
output_registry = OutputRegistry()
class ClusterOutput(Output):
"""Base class for writing/display output of cluster results
severity doesn't apply in this case so exclude those.
"""
def __init__(self, options):
self.filename = options.output_file
def strip_output(self, results):
"""Nothing to strip out"""
return list(results.output())
def generate(self, data):
raise NotImplementedError
@output_registry
class Ansible(ClusterOutput):
"""Output information JSON format for consumption by Ansible
Required keywords in a Result:
name - unique identifier for the return value
One of these is required:
value - the return value. Type? I dunno yet
error - if an error was returned
"""
options = (
('--indent', dict(dest='indent', type=int, default=2,
help='Indention level of JSON output')),
)
def __init__(self, options):
super().__init__(options)
self.indent = options.indent
def generate(self, data):
output = []
for line in data:
kw = line.get('kw')
name = kw.get('name')
value = kw.get('value')
error = kw.get('error')
if value and error:
value = '%s: %s' % (error, value)
elif error:
value = error
rval = {'%s' % name: value}
output.append(rval)
output = json.dumps(output, indent=self.indent)
if self.filename is None:
output += '\n'
return output
freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/ 0000775 0000000 0000000 00000000000 14200534377 0022024 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024123 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/crlmanager.py 0000664 0000000 0000000 00000002374 14200534377 0024517 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipaclustercheck.ipa.plugin import ClusterPlugin, registry, find_checks
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
@registry
class ClusterCRLManagerCheck(ClusterPlugin):
@duration
def check(self):
data = self.registry.json
crlmanagers = []
for fqdn in data.keys():
output = find_checks(data[fqdn], 'ipahealthcheck.ipa.roles',
'IPACRLManagerCheck')
enabled = output[0].get('kw').get('crlgen_enabled')
if enabled:
crlmanagers.append(fqdn)
if len(crlmanagers) == 0:
yield Result(self, constants.ERROR,
name='crlmanager',
error='No CRL Manager defined')
elif len(crlmanagers) == 1:
yield Result(self, constants.SUCCESS,
name='crlmanager',
value=crlmanagers[0])
else:
yield Result(self, constants.ERROR,
name='crlmanager',
value=','.join(crlmanagers),
error='Multiple CRL Managers defined')
freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/plugin.py 0000664 0000000 0000000 00000006533 14200534377 0023703 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from copy import deepcopy
import json
import logging
from os import listdir
from os.path import isfile, join
from ipahealthcheck.core.plugin import Plugin, Registry
from ipalib import api
logger = logging.getLogger()
def find_checks(data, source, check):
"""Look through the dict for a matching source and check.
data: dict of source and check output
source: name of source to find
check: name of check to find
Returns list of contents of source + check or empty list
"""
rval = []
for d in data:
if d.get('source') == source and d.get('check') == check:
rval.append(d)
return rval
def get_masters(data):
"""
Return the list of known masters
This is determined from the list of loaded healthcheck logs. It
is possible that mixed versions are used so some may not be
reporting the full list of masters, so check them all, and raise
an exception if the list cannot be determined.
"""
test_masters = list(data)
masters = None
for master in test_masters:
output = find_checks(data[master], 'ipahealthcheck.ipa.meta',
'IPAMetaCheck')
if len(output) == 0:
raise ValueError('Unable to determine full list of masters. '
'ipahealthcheck.ipa.meta:IPAMetaCheck not '
'found.')
masters = output[0].get('kw').get('masters')
if masters:
return masters
raise ValueError('Unable to determine full list of masters. '
'None of ipahealthcheck.ipa.meta:IPAMetaCheck '
'contain masters.')
class ClusterPlugin(Plugin):
pass
class ClusterRegistry(Registry):
def __init__(self):
super().__init__()
self.json = None
def initialize(self, framework, config, options=None):
super().initialize(framework, config, options)
self.json = {}
self.load_files(options.dir)
if not api.isdone('finalize'):
if not api.isdone('bootstrap'):
api.bootstrap(in_server=True,
context='ipahealthcheck',
log=None)
if not api.isdone('finalize'):
api.finalize()
def load_files(self, dir):
if self.json:
return
files = [f for f in listdir(dir) if isfile(join(dir, f))]
for file in files:
fname = join(dir, file)
logger.debug("Reading %s", fname)
try:
with open(fname, 'r') as fd:
data = fd.read()
except Exception as e:
logger.error("Unable to read %s: %s", fname, e)
continue
try:
data = json.loads(data)
except Exception as e:
logger.error("Unable to parse JSON in %s: %s", fname, e)
continue
meta = find_checks(data, 'ipahealthcheck.meta.core',
'MetaCheck')
if meta:
fqdn = meta[0].get('kw').get('fqdn')
self.json[fqdn] = deepcopy(data)
else:
logger.error("No fqdn defined in JSON in %s", fname)
continue
registry = ClusterRegistry()
freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/ruv.py 0000664 0000000 0000000 00000013171 14200534377 0023215 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipaclustercheck.ipa.plugin import (
ClusterPlugin,
registry,
find_checks,
get_masters
)
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
from ipalib import api
from ipapython.dn import DN
logger = logging.getLogger()
@registry
class ClusterRUVCheck(ClusterPlugin):
# TODO: confirm that all masters are represented, otherwise the
# trustworthiness of dangling RUV is mixed.
#
# gah, need to provide full list of all masters in a check.
@duration
def check(self):
data = self.registry.json
# Start with the list of masters from the file(s) collected
# and find a MetaCheck with a full list of masters. For
# backwards compatibility.
try:
masters = get_masters(data)
except ValueError as e:
yield Result(self, constants.ERROR,
name='dangling_ruv',
error=str(e))
return
if len(data.keys()) < len(masters):
yield Result(self, constants.ERROR,
name='dangling_ruv',
error='Unable to determine list of RUVs, missing '
'some masters: %s' %
''.join(set(masters) - set(data.keys())))
return
# collect the full set of known RUVs for each master
info = {}
for master in masters:
info[master] = {
'ca': False, # does the host have ca configured?
'ruvs': set(), # ruvs on the host
'csruvs': set(), # csruvs on the host
'clean_ruv': set(), # ruvs to be cleaned from the host
'clean_csruv': set() # csruvs to be cleaned from the host
}
for fqdn in data.keys():
outputs = find_checks(data[fqdn], 'ipahealthcheck.ds.ruv',
'KnownRUVCheck')
for output in outputs:
if not 'suffix' in output.get('kw'):
continue
basedn = DN(output.get('kw').get('suffix'))
ruvset = set()
ruvtmp = output.get('kw').get('ruvs')
for ruv in ruvtmp:
ruvset.add(tuple(ruv))
if basedn == DN('o=ipaca'):
info[fqdn]['ca'] = True
info[fqdn]['csruvs'] = ruvset
elif basedn == api.env.basedn:
info[fqdn]['ruvs'] = ruvset
else:
yield Result(self, constants.WARNING,
name='dangling_ruv',
error='Unknown suffix found %s expected %s'
% (basedn, api.env.basedn))
# Collect the nsDS5ReplicaID for each master
ruvs = set()
csruvs = set()
for fqdn in data.keys():
outputs = find_checks(data[fqdn], 'ipahealthcheck.ds.ruv',
'RUVCheck')
for output in outputs:
if not 'key' in output.get('kw'):
continue
basedn = DN(output.get('kw').get('key'))
ruv = (fqdn, (output.get('kw').get('ruv')))
if basedn == DN('o=ipaca'):
csruvs.add(ruv)
elif basedn == api.env.basedn:
ruvs.add(ruv)
else:
yield Result(self, constants.WARNING,
name='dangling_ruv',
error='Unknown suffix found %s expected %s'
% (basedn, api.env.basedn))
dangles = False
# get the dangling RUVs
for master_info in info.values():
for ruv in master_info['ruvs']:
if ruv not in ruvs:
master_info['clean_ruv'].add(ruv)
dangles = True
# if ca is not configured, there will be no csruvs in master_info
for csruv in master_info['csruvs']:
if csruv not in csruvs:
master_info['clean_csruv'].add(csruv)
dangles = True
clean_csruvs = set()
clean_ruvs = set()
if dangles:
for _unused, master_info in info.items():
for ruv in master_info['clean_ruv']:
logger.debug(
"Dangling RUV id: %s, hostname: %s", ruv[1], ruv[0]
)
clean_ruvs.add(ruv[1])
for csruv in master_info['clean_csruv']:
logger.debug(
"Dangling CS RUV id: %s, hostname: %s",
csruv[1],
csruv[0]
)
clean_csruvs.add(csruv[1])
if clean_ruvs:
yield Result(self, constants.ERROR,
name='dangling_ruv',
value=', '.join(clean_ruvs))
else:
yield Result(self, constants.SUCCESS,
name='dangling_ruv',
value='No dangling RUVs found')
if clean_csruvs:
yield Result(self, constants.ERROR,
name='dangling_csruv',
value=', '.join(clean_csruvs))
else:
yield Result(self, constants.SUCCESS,
name='dangling_csruv',
value='No dangling CS RUVs found')
freeipa-healthcheck-0.10/src/ipahealthcheck/ 0000775 0000000 0000000 00000000000 14200534377 0021037 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/__init__.py 0000664 0000000 0000000 00000000177 14200534377 0023155 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
__import__('pkg_resources').declare_namespace(__name__)
freeipa-healthcheck-0.10/src/ipahealthcheck/core/ 0000775 0000000 0000000 00000000000 14200534377 0021767 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/core/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024066 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/core/config.py 0000664 0000000 0000000 00000006630 14200534377 0023613 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
import os
from configparser import ConfigParser, ParsingError
from collections import OrderedDict
from ipahealthcheck.core.constants import CONFIG_SECTION, EXCLUDE_SECTION
from ipahealthcheck.core.constants import DEFAULT_CONFIG
logger = logging.getLogger()
class DuplicateOrderedDict(OrderedDict):
def __setitem__(self, key, value):
"""Duplicate keys will be concatenated strings separated by new-line"""
if isinstance(value, list) and key in self:
self[key].extend(value)
else:
super().__setitem__(key, value)
class Config:
"""Helper class to manage configuration
Let one treat config items as properties instead of using
a dict. It just allows for an easier-to-read shorthand.
>>> config = Config()
>>> config.foo = 'bar'
>>> config.foo
'bar'
Return a list of the configuration option keys.
>>> list(config)
['foo']
"""
def __init__(self):
object.__setattr__(self, '_Config__d', {})
def __setattr__(self, key, value):
"""
Set the attribute named ``name`` to ``value``.
"""
self[key] = value
def __setitem__(self, key, value):
"""
Set ``key`` to ``value``.
"""
object.__setattr__(self, key, value)
self.__d[key] = value
def __getattr__(self, key):
"""
Return the value corresponding to ``key``.
"""
return self.__d[key]
def __getitem__(self, key):
"""
Return the value corresponding to ``key``.
"""
return self.__d[key]
def __contains__(self, key):
"""
Return True if instance contains ``key``; otherwise return False.
"""
return key in self.__d
def __iter__(self):
"""
Iterate through keys in ascending order.
"""
for key in sorted(self.__d):
yield key
def merge(self, d):
"""
Merge variables from dict ``d`` into the configuration
The first one wins.
:param d: dict containing configuration
"""
for key in d:
self.__d[key] = d[key]
def read_config(config_file):
"""
Simple configuration file reader
Read and return the configuration for only the default section.
Returns a dict on success, None on failure
"""
config = Config()
config.merge(DEFAULT_CONFIG)
if not os.path.exists(config_file):
logging.warning(
"config file %s does not exist, using defaults", config_file
)
return config
parser = ConfigParser(dict_type=DuplicateOrderedDict, strict=False)
try:
parser.read(config_file)
except ParsingError as e:
logging.error("Unable to parse %s: %s", config_file, e)
return None
if not parser.has_section(CONFIG_SECTION):
logging.error(
"Config file %s missing %s section", config_file, CONFIG_SECTION
)
return None
items = parser.items(CONFIG_SECTION)
for (key, value) in items:
if not key.startswith('excludes_'):
config[key] = value
if parser.has_section(EXCLUDE_SECTION):
items = parser.items(EXCLUDE_SECTION)
for (key, value) in items:
config[EXCLUDE_SECTION + '_' + key] = value.split(os.linesep)
return config
freeipa-healthcheck-0.10/src/ipahealthcheck/core/constants.py 0000664 0000000 0000000 00000002554 14200534377 0024363 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
DEFAULT_OUTPUT = 'json'
# Error reporting result
SUCCESS = 0
WARNING = 10
ERROR = 20
CRITICAL = 30
_levelToName = {
SUCCESS: 'SUCCESS',
WARNING: 'WARNING',
ERROR: 'ERROR',
CRITICAL: 'CRITICAL',
}
_nameToLevel = {
'SUCCESS': SUCCESS,
'WARNING': WARNING,
'ERROR': ERROR,
'CRITICAL': CRITICAL,
}
def getLevelName(level):
"""
Translate between level constants and their textual mappings.
If the level is one of the predefined levels then returns the
corresponding string.
If a numeric value corresponding to one of the defined levels
is passed in instead the corresponding string representation is
returned.
"""
name = _levelToName.get(level) or _nameToLevel.get(level)
if name is not None:
return name
return level
def getLevel(name):
"""
Translate between level text and their numeric constants
If the level is one of the predefined levels then returns the
corresponding number.
"""
level = _nameToLevel.get(name)
if level is not None:
return level
return name
CONFIG_FILE = '/etc/ipahealthcheck/ipahealthcheck.conf'
CONFIG_SECTION = 'default'
EXCLUDE_SECTION = 'excludes'
DEFAULT_TIMEOUT = 10
DEFAULT_CONFIG = {
'cert_expiration_days': 28,
'timeout': DEFAULT_TIMEOUT,
}
freeipa-healthcheck-0.10/src/ipahealthcheck/core/core.py 0000664 0000000 0000000 00000040136 14200534377 0023275 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import argparse
import json
import logging
import pkg_resources
import signal
import warnings
import traceback
from datetime import datetime
from ipahealthcheck.core.config import read_config
from ipahealthcheck.core.exceptions import TimeoutError
from ipahealthcheck.core.plugin import Result, Results, json_to_results
from ipahealthcheck.core.output import output_registry
from ipahealthcheck.core import constants
from ipahealthcheck.core.service import ServiceCheck
logging.basicConfig(format='%(message)s')
logger = logging.getLogger()
def find_registries(entry_points):
# Loading the resources may reset the log level, save it.
log_level = logger.level
registries = {}
for entry_point in entry_points:
registries.update({
ep.name: ep.resolve()
for ep in pkg_resources.iter_entry_points(entry_point)
})
logger.setLevel(log_level)
return registries
def find_plugins(name, registry):
for ep in pkg_resources.iter_entry_points(name):
# load module
ep.load()
return registry.get_plugins()
def run_plugin(plugin, available=(), timeout=constants.DEFAULT_TIMEOUT):
def signal_handler(signum, frame):
raise TimeoutError('Request timed out')
# manually calculate duration when we create results of our own
start = datetime.utcnow()
signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(timeout)
try:
for result in plugin.check():
if result is None:
# Treat no result as success, fudge start time
result = Result(plugin, constants.SUCCESS, start=start)
yield result
except TimeoutError as e:
yield Result(plugin, constants.ERROR, exception=str(e),
start=start)
except Exception as e:
logger.debug('Exception raised: %s', e)
logger.debug(traceback.format_exc())
yield Result(plugin, constants.CRITICAL, exception=str(e),
traceback=traceback.format_exc(),
start=start)
finally:
signal.alarm(0)
def source_or_check_matches(plugin, source, check):
"""Determine whether a given a plugin matches if a source
and optional check are provided.
"""
if (
source is not None and
not _is_prefix_of_source(source, plugin.__module__)
):
return False
if check and plugin.__class__.__name__ != check:
return False
return True
def exclude_source_or_check(source, check, config):
"""Return True if a source or check should be excluded, otherwise False"""
exclude_source = []
exclude_check = []
if 'excludes_source' in config:
exclude_source = config.excludes_source
if 'excludes_check' in config:
exclude_check = config.excludes_check
for exclude in exclude_source:
if _is_prefix_of_source(exclude, source):
return True
for exclude in exclude_check:
if exclude == check:
return True
return False
def run_service_plugins(plugins, source, check):
"""Execute plugins with the base class of ServiceCheck
This is a specialized check to use systemd to determine
if a service is running or not.
"""
results = Results()
available = []
for plugin in plugins:
if not isinstance(plugin, ServiceCheck):
continue
# Try to save some time to not check dependent services if the
# parent is down.
if not set(plugin.requires).issubset(available):
# A required service is not available. Either it hasn't been
# checked yet or it isn't running. If not running break.
running = True
for result in results.results:
if result.check in plugin.requires:
# if not in available but in results the service failed
running = False
break
if not running:
logger.debug(
'Skipping %s:%s because %s service(s) not running',
plugin.__class__.__module__,
plugin.__class__.__name__,
', '.join(set(plugin.requires) - set(available))
)
continue
logger.debug('Calling check %s', plugin)
for result in plugin.check():
# always run the service checks so dependencies work
if result is not None and result.result == constants.SUCCESS:
available.append(plugin.service.service_name)
if not source_or_check_matches(plugin, source, check):
continue
if result is not None:
results.add(result)
return results, set(available)
def run_plugins(plugins, available, source, check,
config, timeout=constants.DEFAULT_TIMEOUT):
"""Execute plugins without the base class of ServiceCheck
These are the remaining, non-service checking checks
that do validation for various parts of a system.
"""
results = Results()
for plugin in plugins:
if isinstance(plugin, ServiceCheck):
continue
if exclude_source_or_check(
plugin.__module__, plugin.__class__.__name__, config
):
logger.debug("Excluding %s::%s per config",
plugin.__module__, plugin.__class__.__name__)
continue
if not source_or_check_matches(plugin, source, check):
continue
logger.debug("Calling check %s", plugin)
if not set(plugin.requires).issubset(available):
logger.debug('Skipping %s:%s because %s service(s) not running',
plugin.__class__.__module__,
plugin.__class__.__name__,
', '.join(set(plugin.requires) - available))
# Not providing a Result in this case because if a required
# service isn't available then this could generate a lot of
# false positives.
else:
for result in run_plugin(plugin, available, timeout):
results.add(result)
return results
def list_sources(plugins):
"""Print list of all sources and checks"""
source = None
for plugin in plugins:
if source != plugin.__class__.__module__:
print(plugin.__class__.__module__)
source = plugin.__class__.__module__
print(" ", plugin.__class__.__name__)
return 0
def add_default_options(parser, output_registry, default_output):
output_names = [plugin.__name__.lower() for
plugin in output_registry.plugins]
parser.add_argument('--config', dest='config',
default=None, help='Config file to load')
parser.add_argument('--verbose', dest='verbose', action='store_true',
default=False, help='Run in verbose mode')
parser.add_argument('--debug', dest='debug', action='store_true',
default=False, help='Include debug output')
parser.add_argument('--list-sources', dest='list_sources',
action='store_true', default=False,
help='List all available sources')
parser.add_argument('--source', dest='source',
default=None,
help='Source of checks, e.g. foo.bar.baz')
parser.add_argument('--check', dest='check',
default=None,
help='Check to execute, e.g. BazCheck')
parser.add_argument('--output-type', dest='output_type',
choices=output_names,
default=default_output, help='Output method')
parser.add_argument('--output-file', dest='output_file', default=None,
help='File to store output')
parser.add_argument('--version', dest='version', action='store_true',
help='Report the version number and exit')
def add_output_options(parser, output_registry):
for plugin in output_registry.plugins:
onelinedoc = plugin.__doc__.split('\n\n', 1)[0].strip()
group = parser.add_argument_group(plugin.__name__.lower(),
onelinedoc)
for option in plugin.options:
group.add_argument(option[0], **option[1])
def parse_options(parser):
options = parser.parse_args()
# Validation
if options.check and not options.source:
print("--source is required when --check is used")
return 1
return options
def limit_results(results, source, check):
"""Return ony those results which match source and/or check"""
new_results = Results()
for result in results.results:
if check is None:
# treat 'source' as prefix
if _is_prefix_of_source(source, result.source):
new_results.add(result)
else:
# when 'check' is given, match source fully
if result.source == source and result.check == check:
new_results.add(result)
return new_results
def exclude_keys(config, results):
"""Generate a new result, excluding unwanted keys"""
new_results = Results()
for result in results.results:
if (
'excludes_key' in config and
result.kw.get("key") in config.excludes_key
):
logger.debug("Excluding %s::%s::%s per config",
result.source, result.check, result.kw.get('key'))
else:
new_results.add(result)
return new_results
def _is_prefix_of_source(prefix, source):
prefix_parts = prefix.split('.')
source_parts = source.split('.')
return source_parts[:len(prefix_parts)] == prefix_parts
class RunChecks:
def __init__(self, entry_points, configfile,
output_registry=output_registry,
default_output='json'):
"""Initialize class variables
entry_points: A list of entry points to find plugins
configfile: full path to the config file
output_registry: registry containing the set of output
plugins to register.
default_output: default output class
"""
self.entry_points = entry_points
self.configfile = configfile
self.output_registry = output_registry
self.default_output = default_output
self.parser = argparse.ArgumentParser()
self.options = None
def pre_check(self):
return None
def add_options(self):
"""Add custom options for this check program"""
def validate_options(self):
"""Validate options other than source and check"""
return None
def run_healthcheck(self):
framework = object()
plugins = []
output = constants.DEFAULT_OUTPUT
logger.setLevel(logging.WARNING)
add_default_options(self.parser, self.output_registry,
self.default_output)
add_output_options(self.parser, self.output_registry)
self.add_options()
options = parse_options(self.parser)
if options.version:
for registry in self.entry_points:
name = registry.split('.')[0]
try:
version = pkg_resources.get_distribution(name).version
except pkg_resources.DistributionNotFound:
continue
print('%s: %s' % (name, version))
return 0
# pylint: disable=assignment-from-none
rval = self.validate_options()
# pylint: enable=assignment-from-none
if rval is not None:
return rval
if options.verbose:
logger.setLevel(logging.INFO)
if options.debug:
logger.setLevel(logging.DEBUG)
if options.config is not None:
config = read_config(options.config)
else:
config = read_config(self.configfile)
if config is None:
return 1
# Unify config and options. One of these variables will be
# eventually deprecated in the future. This way all cli
# options can be set in config instead.
config.merge(vars(options))
self.options = config
options = config
# pylint: disable=assignment-from-none
rval = self.pre_check()
# pylint: enable=assignment-from-none
if rval is not None:
return rval
# If we have IPA configured without a CA then we want to skip
# the pkihealthcheck plugins otherwise they will generated a
# lot of false positives. The IPA plugins are loaded first so
# which should set ca_configured in its registry to True or
# False. We will skip the pkihealthcheck plugins only if
# ca_configured is False which means that it was set by IPA.
ca_configured = False
for name, registry in find_registries(self.entry_points).items():
try:
registry.initialize(framework, config, options)
except Exception as e:
warnings.warn("Trying deprecated initialization API: %s" % e,
DeprecationWarning)
try:
registry.initialize(framework, config)
except Exception as e:
logger.error("Unable to initialize %s: %s", name, e)
continue
if hasattr(registry, 'ca_configured'):
ca_configured = registry.ca_configured
for name, registry in find_registries(self.entry_points).items():
if 'pkihealthcheck' in name and ca_configured is False:
logger.debug('IPA CA is not configured, skipping %s', name)
continue
for plugin in find_plugins(name, registry):
plugins.append(plugin)
for out in self.output_registry.plugins:
if out.__name__.lower() == options.output_type:
output = out(options)
break
if options.list_sources:
return list_sources(plugins)
if 'infile' in options and options.infile:
try:
with open(options.infile, 'r') as f:
raw_data = f.read()
json_data = json.loads(raw_data)
results = json_to_results(json_data)
available = ()
except Exception as e:
print("Unable to import '%s': %s" % (options.infile, e))
return 1
if options.source:
results = limit_results(results, options.source, options.check)
else:
results, available = run_service_plugins(plugins,
options.source,
options.check)
results.extend(run_plugins(plugins, available,
options.source, options.check, config,
int(config.timeout)))
if options.source and len(results.results) == 0:
for plugin in plugins:
if not source_or_check_matches(plugin, options.source,
options.check):
continue
if not set(plugin.requires).issubset(available):
print("Source '%s' is missing one or more requirements "
"'%s'" %
(options.source, ', '.join(plugin.requires)))
return 1
if options.check:
print("Check '%s' not found in Source '%s'" %
(options.check, options.source))
else:
print("Source '%s' not found" % options.source)
return 1
results = exclude_keys(config, results)
try:
output.render(results)
except Exception as e:
logger.error('Output raised %s: %s', e.__class__.__name__, e)
return_value = 0
for result in results.results:
if result.result != constants.SUCCESS:
return_value = 1
break
return return_value
freeipa-healthcheck-0.10/src/ipahealthcheck/core/exceptions.py 0000664 0000000 0000000 00000000157 14200534377 0024525 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
class TimeoutError(Exception):
pass
freeipa-healthcheck-0.10/src/ipahealthcheck/core/files.py 0000664 0000000 0000000 00000011142 14200534377 0023442 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import grp
import os
import pwd
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Result, duration
class FileCheck:
"""Generic check to validate permission and ownership of files
files is a tuple of tuples. Each tuple consists of:
(path, expected_perm, expected_owner, expected_group)
perm is in the form of a POSIX ACL: e.g. 0440, 0770.
owner and group are names or a tuple of names, not uid/gid.
If owner and/or group are tuples then all names are checked.
If a match is found that that is the one reported in SUCCESS.
If it fails then all values are reported.
"""
def __init__(self):
self.files = []
@duration
def check(self):
for (path, owner, group, mode) in self.files:
if not isinstance(owner, tuple):
owner = tuple((owner,))
if not isinstance(group, tuple):
group = tuple((group,))
if not os.path.exists(path):
for type in ('mode', 'owner', 'group'):
key = '%s_%s' % (path.replace('/', '_'), type)
yield Result(self, constants.SUCCESS, key=key,
type=type, path=path,
msg='File does not exist')
continue
stat = os.stat(path)
fmode = str(oct(stat.st_mode)[-4:])
key = '%s_mode' % path.replace('/', '_')
if mode != fmode:
if mode < fmode:
yield Result(self, constants.WARNING, key=key,
path=path, type='mode', expected=mode,
got=fmode,
msg='Permissions of %s are too permissive: '
'%s and should be %s' % (path, fmode, mode))
if mode > fmode:
yield Result(self, constants.ERROR, key=key,
path=path, type='mode', expected=mode,
got=fmode,
msg='Permissions of %s are too restrictive: '
'%s and should be %s' % (path, fmode, mode))
else:
yield Result(self, constants.SUCCESS, key=key,
type='mode', path=path)
found = False
for o in owner:
fowner = pwd.getpwnam(o)
if fowner.pw_uid == stat.st_uid:
found = True
break
if not found:
actual = pwd.getpwuid(stat.st_uid)
key = '%s_owner' % path.replace('/', '_')
if len(owner) == 1:
msg = 'Ownership of %s is %s and should ' \
'be %s' % \
(path, actual.pw_name, owner[0])
else:
msg = 'Ownership of %s is %s and should ' \
'be one of %s' % \
(path, actual.pw_name, ','.join(owner))
owner = ','.join(owner)
yield Result(self, constants.WARNING, key=key,
path=path, type='owner', expected=owner,
got=actual.pw_name,
msg=msg)
else:
yield Result(self, constants.SUCCESS, key=key,
type='owner', path=path)
found = False
for g in group:
fgroup = grp.getgrnam(g)
if fgroup.gr_gid == stat.st_gid:
found = True
break
if not found:
key = '%s_group' % path.replace('/', '_')
actual = grp.getgrgid(stat.st_gid)
if len(group) == 1:
msg = 'Group of %s is %s and should ' \
'be %s' % \
(path, actual.gr_name, group[0])
else:
msg = 'Group of %s is %s and should ' \
'be one of %s' % \
(path, actual.gr_name, ','.join(group))
group = ','.join(group)
yield Result(self, constants.WARNING, key=key,
path=path, type='group', expected=group,
got=actual.gr_name,
msg=msg)
else:
yield Result(self, constants.SUCCESS, key=key,
type='group', path=path)
freeipa-healthcheck-0.10/src/ipahealthcheck/core/main.py 0000664 0000000 0000000 00000003302 14200534377 0023263 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from os import environ
import sys
from ipahealthcheck.core import constants
from ipahealthcheck.core.core import RunChecks
try:
from ipalib.facts import is_ipa_configured
except ImportError:
is_ipa_configured = None
class IPAChecks(RunChecks):
def pre_check(self):
if is_ipa_configured is None:
print("IPA server is not installed")
return 1
if not is_ipa_configured():
print("IPA server is not configured")
return 1
return None
def add_options(self):
parser = self.parser
parser.add_argument('--input-file', dest='infile',
help='File to read as input')
parser.add_argument('--failures-only', dest='failures_only',
action='store_true', default=False,
help='Exclude SUCCESS results on output (see'
'man page for more details)')
parser.add_argument('--all', dest='all',
action='store_true', default=False,
help='Report all results on output')
parser.add_argument('--severity', dest='severity', action="append",
help='Include only the selected severity(s)',
choices=list(constants._nameToLevel))
def main():
environ["KRB5_CLIENT_KTNAME"] = "/etc/krb5.keytab"
environ["KRB5CCNAME"] = "MEMORY:"
ipachecks = IPAChecks(['ipahealthcheck.registry',
'pkihealthcheck.registry'],
constants.CONFIG_FILE)
sys.exit(ipachecks.run_healthcheck())
freeipa-healthcheck-0.10/src/ipahealthcheck/core/output.py 0000664 0000000 0000000 00000017221 14200534377 0023704 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from datetime import datetime
import json
import sys
from ipahealthcheck.core.constants import _nameToLevel, SUCCESS
from ipahealthcheck.core.plugin import Registry
class OutputRegistry(Registry):
pass
output_registry = OutputRegistry()
class Output:
"""Base class for writing/displaying the output of results
options is a tuple of argparse options that can add
class-specific options for output.
Output will be typically generated like:
>>> output = JSON(options)
>>> output.render(results)
render() will:
1. Strip out any SUCCESS if requested (strip_output)
2. Generate a string to be written (generate)
3. Write to the requested file or stdout (write_file)
stdout == sys.stdout by default.
An Output class only needs to implement the generate() method
which will render the results into a string for writing.
"""
def __init__(self, options):
self.filename = options.output_file
# Non-required options in the framework, set logical defaults to
# pre 0.6 behavior with everything reported.
self.severity = None
self.failures_only = False
self.all = True
if 'failures_only' in options:
self.failures_only = options.failures_only
if 'all' in options:
self.all = options.all
if 'severity' in options:
self.severity = options.severity
def render(self, results):
"""Process the results into output"""
output = self.strip_output(results)
output = self.generate(output)
self.write_file(output)
def write_file(self, output):
"""Write the output to a file or sys.stdout"""
if self.filename:
with open(self.filename, 'w') as fd:
fd.write(output)
else:
sys.stdout.write(output)
def strip_output(self, results):
"""Strip out SUCCESS results if --failures-only or
--severity was used
Returns a list of result values.
"""
output = []
for line in results.output():
result = line.get('result')
if _nameToLevel.get(result) == SUCCESS:
if self.failures_only:
continue
if (not self.all and
self.filename is None and
not (self.failures_only is False and
not sys.stdin.isatty())):
continue
if self.severity is not None and result not in self.severity:
continue
output.append(line)
return output
def generate(self, data):
"""Convert the output to the desired format, ready for writing
This is the only method an output plugin is required to
provide. The return value should be in ready-to-write format.
Returns a string.
"""
raise NotImplementedError
@output_registry
class JSON(Output):
"""Output information in JSON format"""
options = (
('--indent', dict(dest='indent', type=int, default=2,
help='Indention level of JSON output')),
)
def __init__(self, options):
super().__init__(options)
self.indent = int(options.indent)
def generate(self, data):
output = json.dumps(data, indent=self.indent)
if self.filename is None:
output += '\n'
return output
@output_registry
class Human(Output):
"""Display output in a more human-friendly way"""
options = ()
def generate(self, data):
if not data:
return "No issues found.\n"
output = ''
for line in data:
kw = line.get('kw')
result = line.get('result')
source = line.get('source')
check = line.get('check')
outline = '%s: %s.%s' % (result, source, check)
if 'key' in kw:
outline += '.%s' % kw.get('key')
if 'msg' in kw:
msg = kw.get('msg')
err = msg.format(**kw)
outline += ': %s' % err
elif 'exception' in kw:
outline += ': %s' % kw.get('exception')
output += outline + '\n'
return output
@output_registry
class Prometheus(Output):
"""Render results as Prometheus text metric exposition format"""
options = (
('--metric-prefix', dict(dest='metric_prefix', default='ipa',
help='Metric name prefix')),
)
def __init__(self, options):
super().__init__(options)
self.metric_prefix = options.metric_prefix
def generate(self, data):
if not data:
return '\n'
crt = {}
svc = {}
chk = {}
for line in data:
kw = line.get('kw')
result = line.get('result')
source = line.get('source')
check = line.get('check')
if result in chk:
chk[result] += 1
else:
chk[result] = 1
if source == 'ipahealthcheck.meta.services':
state = 1.0 if _nameToLevel.get(result) == SUCCESS else 0.0
svc[check] = state
elif (source == 'ipahealthcheck.ipa.certs' and
check == "IPACertmongerExpirationCheck"):
# only unsuccessful checks carry the expiration information
if 'key' in kw and 'expiration_date' in kw:
expiration = datetime.strptime(kw['expiration_date'],
'%Y%m%d%H%M%SZ')
crt[kw['key']] = expiration.timestamp()
metrics = []
self.generate_check_metrics(metrics, chk)
self.generate_service_metrics(metrics, svc)
self.generate_certificate_metrics(metrics, crt)
metrics.append('')
return "\n".join(metrics)
def generate_check_metrics(self, out, data):
if not data:
return
metric_name = 'healthcheck'
out.append('HELP %s_%s %s' %
(self.metric_prefix, metric_name,
'Number of healthchecks with a certain result'))
out.append('TYPE %s_%s gauge' %
(self.metric_prefix, metric_name))
for check, quantity in data.items():
out.append('%s_%s{result="%s"} %.1f' %
(self.metric_prefix, metric_name,
check, quantity))
def generate_service_metrics(self, out, data):
if not data:
return
metric_name = 'service_state'
out.append('HELP %s_%s %s' %
(self.metric_prefix, metric_name,
'State of the services monitored by IPA healthcheck'))
out.append('TYPE %s_%s gauge' %
(self.metric_prefix, metric_name))
for service, state in data.items():
out.append('%s_%s{service="%s"} %.1f' %
(self.metric_prefix, metric_name,
service, state))
def generate_certificate_metrics(self, out, data):
if not data:
return
metric_name = 'cert_expiration'
out.append('HELP %s_%s %s' %
(self.metric_prefix, metric_name,
'Expiration date of certificates in warning/error state'))
out.append('TYPE %s_%s gauge' %
(self.metric_prefix, metric_name))
for certificate, timestamp in data.items():
out.append('%s_%s{certificate_request_id="%s"} %.9e' %
(self.metric_prefix, metric_name,
certificate, timestamp))
freeipa-healthcheck-0.10/src/ipahealthcheck/core/plugin.py 0000664 0000000 0000000 00000014745 14200534377 0023652 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import uuid
from datetime import datetime
from functools import wraps
from ipahealthcheck.core.constants import getLevelName, getLevel
def duration(f):
"""Compute the duration of execution"""
@wraps(f)
def wrapper(*args, **kwds):
start = datetime.utcnow()
end = None
for result in f(*args, **kwds):
end = datetime.utcnow()
dur = end - start
result.duration = '%6.6f' % dur.total_seconds()
yield result
if end is None:
# no results, yield None so a SUCCESS result will be created
yield None
return wrapper
class Registry:
"""
A decorator that makes plugins available to the API
Usage::
register = Registry()
@register()
class some_plugin(...):
...
"""
def __init__(self):
self.plugins = []
self.framework = None
self.config = dict()
self.options = None
def initialize(self, framework, config, options=None):
self.framework = framework
self.config = config
self.options = options
def __call__(self, cls):
if not callable(cls):
raise TypeError('plugin must be callable; got %r' % cls)
self.plugins.append(cls)
return cls
def get_plugins(self):
for plugincls in self.plugins:
yield plugincls(self)
class Plugin:
"""
Base class for all plugins.
registry defines where the plugin was registered, normally via
a pkg_resource.
requires is a tuple of strings that define pre-requisites for
execution. Some output formats allow plugins that do not have
these requirements met to skip them (JSON does NOT, all plugins
are always executed and reported).
Each Plugin should define a check() method that contains as
simple a test as possible on the status a unique potential issue.
A Plugin may return either Result for a single result or
Results if multiple issues are discovered.
It is strongly recommended to keep each Plugin as discrete as
possible. This is not always possible or practical, for example
to avoid hundreds of plugins that test nearly the same thing.
Usage::
register = Registry()
@register()
tmp_exists_check(Plugin)
def check(self):
if os.path.exists('/tmp'):
result = Result(self, SUCCESS)
else:
result = Result(self, CRITICAL, path='/tmp',
msg='Temporary directory is missing')
return result
"""
requires = ()
def __init__(self, registry):
self.registry = registry
self.config = registry.config
class Result:
"""
The result of a check.
:param plugin: The plugin which generated the result.
:param result: A result constant representing the level of error.
:param source: If no plugin is passed then the name of the source
can be provided directly.
:param check: If no plugin is passed then the name of the check
can be provided directly.
:param kw: A dictionary of items providing insight in the error.
Either both check and source need to be provided or plugin needs
to be provided.
kw is meant to provide some level of flexibility to check authors
but the following is a set of pre-defined keys that may be present:
key: some checks can have multiple tests. This
provides for uniqueuess.
msg: A message that can take other keywords as input
exception: used when a check raises an exception
"""
def __init__(self, plugin, result, source=None, check=None,
start=None, duration=None, when=None, **kw):
self.result = result
self.kw = kw
self.when = when or generalized_time(datetime.utcnow())
self.duration = duration
self.uuid = str(uuid.uuid4())
if None not in (check, source):
self.check = check
self.source = source
else:
if plugin is None:
raise TypeError('source and check or plugin must be provided')
self.check = plugin.__class__.__name__
self.source = plugin.__class__.__module__
if start is not None:
dur = datetime.utcnow() - start
self.duration = '%6.6f' % dur.total_seconds()
assert getLevelName(result) is not None
def __repr__(self):
return "%s.%s(%s): %s" % (self.source, self.check, self.kw,
self.result)
class Results:
"""
A list-like collection of Result values.
Provides a very limited subset of list operations. Is intended for
internal-use only and not by check functions.
Usage::
results = Results()
result = Result(plugin, SUCCESS, **kw)
results.add(result)
"""
def __init__(self):
self.results = []
def __len__(self):
return len(self.results)
def add(self, result):
assert isinstance(result, Result)
self.results.append(result)
def extend(self, results):
assert isinstance(results, Results)
self.results.extend(results.results)
def output(self):
for result in self.results:
yield dict(source=result.source,
check=result.check,
result=getLevelName(result.result),
uuid=result.uuid,
when=result.when,
duration=result.duration,
kw=result.kw)
def json_to_results(data):
"""
Convert JSON data into a Results object.
:param data: valid JSON input
:returns: a Results object representing the JSON input
"""
results = Results()
for line in data:
result = getLevel(line.pop('result'))
source = line.pop('source')
check = line.pop('check')
duration = line.pop('duration')
when = line.pop('when')
kw = line.pop('kw')
result = Result(None, result, source, check, duration=duration,
when=when, **kw)
results.add(result)
return results
def generalized_time(intime):
"""Convert a datetime.datetime object to LDAP generalized time format
:param intime: a datetime.datetime object
"""
assert isinstance(intime, datetime)
return intime.strftime('%Y%m%d%H%M%SZ')
freeipa-healthcheck-0.10/src/ipahealthcheck/core/service.py 0000664 0000000 0000000 00000000532 14200534377 0024001 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core.plugin import Plugin
class ServiceCheck(Plugin):
def __init__(self, registry):
super().__init__(registry)
self.service = None
self.service_name = None
def check(self, instance=''):
raise NotImplementedError
freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/ 0000775 0000000 0000000 00000000000 14200534377 0022304 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024403 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/ca.py 0000664 0000000 0000000 00000011176 14200534377 0023247 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipahealthcheck.dogtag.plugin import DogtagPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api, errors
from ipaplatform.paths import paths
from ipaserver.install import certs
from ipaserver.install import krainstance
from ipapython.directivesetter import get_directive
from cryptography.hazmat.primitives.serialization import Encoding
logger = logging.getLogger()
@registry
class DogtagCertsConfigCheck(DogtagPlugin):
"""
Compare the cert blob in the NSS database to that stored in CS.cfg
"""
@duration
def check(self):
if not self.ca.is_configured():
logger.debug("No CA configured, skipping dogtag config check")
return
kra = krainstance.KRAInstance(api.env.realm)
blobs = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert',
'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert',
'caSigningCert cert-pki-ca': 'ca.signing.cert',
'subsystemCert cert-pki-ca': 'ca.subsystem.cert',
'Server-Cert cert-pki-ca': 'ca.sslserver.cert'}
# Nicknames to skip because their certs are not in CS.cfg
skip = []
if kra.is_installed():
kra_blobs = {
'transportCert cert-pki-kra':
'ca.connector.KRA.transportCert',
}
blobs.update(kra_blobs)
skip.append('storageCert cert-pki-kra')
skip.append('auditSigningCert cert-pki-kra')
db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR)
for nickname, _trust_flags in db.list_certs():
if nickname in skip:
logger.debug('Skipping nickname %s because it isn\'t in '
'the configuration file')
continue
try:
val = get_directive(paths.CA_CS_CFG_PATH,
blobs[nickname], '=')
except KeyError:
logger.debug("%s not found, assuming 3rd party", nickname)
continue
if val is None:
yield Result(self, constants.ERROR,
key=nickname,
configfile=paths.CA_CS_CFG_PATH,
msg='Certificate %s not found in %s' %
(blobs[nickname], paths.CA_CS_CFG_PATH))
continue
cert = db.get_cert_from_db(nickname)
pem = cert.public_bytes(Encoding.PEM).decode()
pem = pem.replace('\n', '')
pem = pem.replace('-----BEGIN CERTIFICATE-----', '')
pem = pem.replace('-----END CERTIFICATE-----', '')
if pem.strip() != val:
yield Result(self, constants.ERROR,
key=nickname,
directive=blobs[nickname],
configfile=paths.CA_CS_CFG_PATH,
msg='Certificate \'%s\' does not match the value '
'of %s in %s' %
(nickname, blobs[nickname], paths.CA_CS_CFG_PATH))
else:
yield Result(self, constants.SUCCESS,
key=nickname,
configfile=paths.CA_CS_CFG_PATH)
@registry
class DogtagCertsConnectivityCheck(DogtagPlugin):
"""
Test basic connectivity by using cert-show to fetch a cert
"""
requires = ('dirsrv',)
@duration
def check(self):
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping connectivity check')
return
# There is nothing special about cert 1. Even if there is no cert
# serial number 1 but the connection is ok it is considered passing.
try:
api.Command.cert_show(1, all=True)
except errors.CertificateOperationError as e:
if 'not found' not in str(e):
yield Result(self, constants.ERROR,
key='cert_show_1',
msg='Request for certificate failed, %s' %
e)
else:
yield Result(self, constants.SUCCESS)
except Exception as e:
yield Result(self, constants.ERROR,
key='cert_show_1',
msg='Request for certificate failed, %s' %
e)
else:
yield Result(self, constants.SUCCESS)
freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/plugin.py 0000664 0000000 0000000 00000002110 14200534377 0024146 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core.plugin import Plugin, Registry
from ipaserver.install import cainstance
from ipaserver.install import installutils
from ipalib import api, errors
class DogtagPlugin(Plugin):
def __init__(self, reg):
super().__init__(reg)
self.ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host)
class DogtagRegistry(Registry):
def initialize(self, framework, config, options=None):
super().initialize(framework, config)
installutils.check_server_configuration()
if not api.isdone('bootstrap'):
api.bootstrap(in_server=True,
context='ipahealthcheck',
log=None)
if not api.isdone('finalize'):
api.finalize()
if not api.Backend.ldap2.isconnected():
try:
api.Backend.ldap2.connect()
except errors.CCacheError:
pass
except errors.NetworkError:
pass
registry = DogtagRegistry()
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/ 0000775 0000000 0000000 00000000000 14200534377 0021445 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/ds/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0023544 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/ds/backends.py 0000664 0000000 0000000 00000000472 14200534377 0023574 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.backend import Backends
@registry
class BackendsCheck(DSPlugin):
"""
Check all the backends for misconfigurations
"""
check_class = Backends
many = True
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/config.py 0000664 0000000 0000000 00000000435 14200534377 0023266 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.config import Config
@registry
class ConfigCheck(DSPlugin):
"""
Check the DS config for obvious errors
"""
check_class = Config
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/disk_space.py 0000664 0000000 0000000 00000000467 14200534377 0024133 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.monitor import MonitorDiskSpace
@registry
class DiskSpaceCheck(DSPlugin):
"""
Check the all the disks that the DS uses
"""
check_class = MonitorDiskSpace
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/ds_plugins.py 0000664 0000000 0000000 00000000546 14200534377 0024173 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.plugins import ReferentialIntegrityPlugin
@registry
class RIPluginCheck(DSPlugin):
"""
Check that the RI plugin configuration is valid and properly indexed
"""
check_class = ReferentialIntegrityPlugin
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/dse.py 0000664 0000000 0000000 00000000446 14200534377 0022576 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.dseldif import DSEldif
@registry
class DSECheck(DSPlugin):
"""
Check the dse.ldif/cn=config for obvious issues
"""
check_class = DSEldif
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/encryption.py 0000664 0000000 0000000 00000000471 14200534377 0024213 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.config import Encryption
@registry
class EncryptionCheck(DSPlugin):
"""
Check the DS security configuration for obvious errors
"""
check_class = Encryption
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/fs_checks.py 0000664 0000000 0000000 00000000450 14200534377 0023746 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.dseldif import FSChecks
@registry
class FSCheck(DSPlugin):
"""
Check the FS for permissions issues impacting DS
"""
check_class = FSChecks
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/nss_ssl.py 0000664 0000000 0000000 00000000454 14200534377 0023506 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.nss_ssl import NssSsl
@registry
class NssCheck(DSPlugin):
"""
Check the NSS database certificates for expiring issues
"""
check_class = NssSsl
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/plugin.py 0000664 0000000 0000000 00000010430 14200534377 0023313 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipalib import api
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Plugin, Result, Registry, duration
from ipaserver.install import dsinstance, installutils
try:
from ipapython.ipaldap import realm_to_serverid
except ImportError:
from ipaserver.install.installutils import realm_to_serverid
from lib389.cli_base import disconnect_instance, connect_instance
from lib389.properties import SER_LDAP_URL, SER_ROOT_DN
class DSArgs(dict):
"""
Prepare the args to make a dirsrv connection that is compatible with
lib389's Dirsrv object.
"""
def __init__(self, inst):
self.pwdfile = None
self.bindpw = None
self.prompt = False
self.instance = inst
class DSPlugin(Plugin):
requires = ('dirsrv',)
check_class = None
many = False
def __init__(self, registry):
super().__init__(registry)
self.ds = self.ds = dsinstance.DsInstance()
self.conn = api.Backend.ldap2
self.serverid = realm_to_serverid(api.env.realm)
def convertSev(self, ds_severity):
"""Convert lib389 HC severity level to IDM's HC level"""
sev = ds_severity.lower()
if sev == 'high':
return constants.CRITICAL
if sev == 'medium':
return constants.ERROR
return constants.WARNING
def doCheck(self, DSObj, many=False):
"""Perform a healthcheck on a specific DS/lib389 class. First
we need to set up the proper args and dicts to properly connect
to the LDAP server via lib389. Then run the classes' lint
functions.
:param DSObj: a class from lib389 that has built-in lint functions
like: Backends, Replica, Encryption, NssSsl, Config, etc
:returns: a list of Result objects
"""
args = DSArgs(self.serverid)
dsrc_inst = {
'uri': args.instance,
'basedn': None,
'binddn': None,
'bindpw': None,
'saslmech': None,
'tls_cacertdir': None,
'tls_cert': None,
'tls_key': None,
'tls_reqcert': 1,
'starttls': False,
'prompt': False,
'pwdfile': None,
'args': {}
}
dsrc_inst['args'][SER_LDAP_URL] = dsrc_inst['uri']
dsrc_inst['args'][SER_ROOT_DN] = dsrc_inst['binddn']
inst = connect_instance(dsrc_inst=dsrc_inst, verbose=False, args=args)
ds_obj = DSObj(inst)
results = []
if many:
# DS class that has many instances of itself (e.g. Backends)
for clo in ds_obj.list():
result = clo.lint()
if result is not None:
# DS result could be a single or multiple results
if isinstance(result, list):
for single_result in result:
results += single_result
else:
results += result
else:
# Single object always returns a list of results
results = ds_obj.lint()
hc_results = []
if results is not None:
for result in results:
hc_results.append(Result(self,
self.convertSev(result['severity']),
key=result['dsle'],
items=result['items'],
msg=result['detail']))
disconnect_instance(inst)
return hc_results
@duration
def check(self):
results = self.doCheck(self.check_class, self.many)
if len(results) > 0:
for result in results:
yield result
else:
yield Result(self, constants.SUCCESS)
class DSRegistry(Registry):
def initialize(self, framework, config, options=None):
super().initialize(framework, config)
installutils.check_server_configuration()
if not api.isdone('bootstrap'):
api.bootstrap(in_server=True,
context='ipahealthcheck',
log=None)
if not api.isdone('finalize'):
api.finalize()
registry = DSRegistry()
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/replication.py 0000664 0000000 0000000 00000000776 14200534377 0024342 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.ds.plugin import DSPlugin, registry
from lib389.replica import Replica, Changelog5
@registry
class ReplicationCheck(DSPlugin):
"""
Check the agreement status for various states, and check for conflicts
"""
check_class = Replica
@registry
class ReplicationChangelogCheck(DSPlugin):
"""
Check the replication changelog has some sort of trimming configured
"""
check_class = Changelog5
freeipa-healthcheck-0.10/src/ipahealthcheck/ds/ruv.py 0000664 0000000 0000000 00000007143 14200534377 0022640 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
import logging
import re
from urllib.parse import urlparse
from ipahealthcheck.ds.plugin import DSPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api, errors
from ipapython.dn import DN
logger = logging.getLogger()
@registry
class RUVCheck(DSPlugin):
"""
Provide the main and dogtag RUV.
Local analysis is not possible since it requires collecting the
RUV from all masters and healthcheck is limited to only talking
to itself.
"""
requires = ('dirsrv',)
def get_ruv(self, dn):
"""Identify the RUV for a suffix on this master"""
try:
entry = self.conn.get_entry(dn)
except Exception:
return None
else:
return entry.single_value.get('nsDS5ReplicaID')
@duration
def check(self):
ruv = self.get_ruv(DN(('cn', 'replica'), ('cn', api.env.basedn),
('cn', 'mapping tree'), ('cn', 'config')))
csruv = self.get_ruv(DN(('cn', 'replica'), ('cn', 'o=ipaca'),
('cn', 'mapping tree'), ('cn', 'config')))
if ruv is not None:
yield Result(self, constants.SUCCESS,
key=str(api.env.basedn),
ruv=ruv)
if csruv is not None:
yield Result(self, constants.SUCCESS,
key='o=ipaca',
ruv=csruv)
@registry
class KnownRUVCheck(DSPlugin):
"""Return all known RUVs. This can be used to identify "dangling"
RUVs, or left-overs from previous replication agreements.
"""
requires = ('dirsrv',)
def get_all_ruvs(self, suffix):
"""Get all known RUVs on this master
Return the RUV entries as a list of tuples: (hostname, rid)
"""
search_filter = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)' \
'(objectclass=nstombstone))'
try:
entries = self.conn.get_entries(
suffix, self.conn.SCOPE_SUBTREE, search_filter,
['nsds50ruv'])
except errors.NotFound:
logger.debug("No RUV records found.")
return []
# raise NoRUVsFound("No RUV records found.")
servers = []
for e in entries:
for ruv in e['nsds50ruv']:
if ruv.startswith('{replicageneration'):
continue
data = re.match(
r'\{replica (\d+) (ldap://.*:\d+)\}(\s+\w+\s+\w*){0,1}',
ruv
)
if data:
rid = data.group(1)
(
_scheme, netloc, _path, _params, _query, _fragment
) = urlparse(data.group(2))
servers.append((re.sub(r':\d+', '', netloc), rid))
else:
logger.debug("Unable to decode RUV: %s", ruv)
return servers
@duration
def check(self):
ruvs = self.get_all_ruvs(api.env.basedn)
csruvs = self.get_all_ruvs(DN('o=ipaca'))
if ruvs:
yield Result(self, constants.SUCCESS,
key='ruvs_' + str(api.env.basedn),
suffix=str(api.env.basedn),
ruvs=ruvs)
if csruvs:
yield Result(self, constants.SUCCESS,
key='ruvs_o=ipaca',
suffix='o=ipaca',
ruvs=csruvs)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/ 0000775 0000000 0000000 00000000000 14200534377 0021610 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0023707 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/certs.py 0000664 0000000 0000000 00000144313 14200534377 0023310 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from __future__ import division
from datetime import datetime, timezone, timedelta
import itertools
import logging
import os
import socket
import tempfile
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result, generalized_time
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api
from ipalib import errors
from ipalib import x509
from ipalib.install import certmonger
from ipalib.constants import RENEWAL_CA_NAME, IPA_CA_RECORD
from ipaplatform.paths import paths
from ipaserver.install import certs
from ipaserver.install import dsinstance
from ipaserver.install import krainstance
from ipaserver.install import krbinstance
from ipaserver.plugins import ldap2
from ipapython import certdb
from ipapython import ipautil
from ipapython.dn import DN
from ipapython.ipaldap import realm_to_serverid
logger = logging.getLogger()
DAY = 60 * 60 * 24
def is_ipa_issued_cert(myapi, cert):
"""Thin wrapper around certs.is_ipa_issued to test for LDAP"""
if not myapi.Backend.ldap2.isconnected():
return None
return certs.is_ipa_issued_cert(myapi, cert)
def get_expected_requests(ca, ds, serverid):
"""Provide the expected certmonger tracking request data
This list is based in part on certificate_renewal_update() in
ipaserver/install/server/upgrade.py and various
start_tracking_certificates() methods in *instance.py.
The list is filtered depending on whether a CA is running
and the certificates have been issued by IPA.
:param ca: the CAInstance
:param ds: the DSInstance
:param serverid: the DS serverid name
"""
template = paths.CERTMONGER_COMMAND_TEMPLATE
if api.Command.ca_is_enabled()['result']:
requests = [
{
'cert-file': paths.RA_AGENT_PEM,
'key-file': paths.RA_AGENT_KEY,
'ca-name': RENEWAL_CA_NAME,
'cert-presave-command': template % 'renew_ra_cert_pre',
'cert-postsave-command': template % 'renew_ra_cert',
},
]
else:
requests = []
if ca.is_configured():
dogtag_reqs = ca.tracking_reqs.items()
kra = krainstance.KRAInstance(api.env.realm)
if kra.is_installed():
dogtag_reqs = itertools.chain(dogtag_reqs,
kra.tracking_reqs.items())
for nick, profile in dogtag_reqs:
req = {
'cert-database': paths.PKI_TOMCAT_ALIAS_DIR,
'cert-nickname': nick,
'ca-name': RENEWAL_CA_NAME,
'cert-presave-command': template % 'stop_pkicad',
'cert-postsave-command':
(template % 'renew_ca_cert "{}"'.format(nick)),
'template-profile': profile,
}
requests.append(req)
else:
logger.debug('CA is not configured, skipping CA tracking')
cert = x509.load_certificate_from_file(paths.HTTPD_CERT_FILE)
issued = is_ipa_issued_cert(api, cert)
if issued is None:
logger.debug('Unable to determine if \'%s\' was issued by IPA '
'because no LDAP connection, assuming yes.')
if issued or issued is None:
requests.append(
{
'cert-file': paths.HTTPD_CERT_FILE,
'key-file': paths.HTTPD_KEY_FILE,
'ca-name': 'IPA',
'cert-postsave-command': template % 'restart_httpd',
}
)
else:
logger.debug('HTTP cert not issued by IPA, \'%s\', skip tracking '
'check', DN(cert.issuer))
# Check the ldap server cert if issued by IPA
ds_nickname = ds.get_server_cert_nickname(serverid)
ds_db_dirname = dsinstance.config_dirname(serverid)
ds_db = certs.CertDB(api.env.realm, nssdir=ds_db_dirname)
connected = api.Backend.ldap2.isconnected()
if not connected:
logger.debug('Unable to determine if \'%s\' was issued by IPA '
'because no LDAP connection, assuming yes.')
if not connected or ds_db.is_ipa_issued_cert(api, ds_nickname):
requests.append(
{
'cert-database': ds_db_dirname[:-1],
'cert-nickname': ds_nickname,
'ca-name': 'IPA',
'cert-postsave-command':
'%s %s' % (template % 'restart_dirsrv', serverid),
}
)
else:
logger.debug('DS cert is not issued by IPA, skip tracking check')
# Check if pkinit is enabled
if os.path.exists(paths.KDC_CERT):
pkinit_request_ca = krbinstance.get_pkinit_request_ca()
requests.append(
{
'cert-file': paths.KDC_CERT,
'key-file': paths.KDC_KEY,
'ca-name': pkinit_request_ca,
'cert-postsave-command':
template % 'renew_kdc_cert',
}
)
else:
logger.debug('No KDC pkinit certificate')
# See if a host certificate was issued. This is only to
# prevent a false-positive if one is indeed installed.
local = {
paths.IPA_NSSDB_DIR: 'Local IPA host',
paths.NSS_DB_DIR: 'IPA Machine Certificate - %s' % socket.getfqdn(),
}
for db, nickname in local.items():
nssdb = certdb.NSSDatabase(db)
if nssdb.has_nickname(nickname):
requests.append(
{
'cert-database': db,
'cert-nickname': nickname,
'ca-name': 'IPA',
}
)
return requests
def get_dogtag_cert_password():
"""Return the NSSDB token password
Will raise IOError if there is a problem reading the file.
"""
ca_passwd = None
token = 'internal'
with open(paths.PKI_TOMCAT_PASSWORD_CONF, 'r') as f:
for line in f:
(tok, pin) = line.split('=', 1)
if token == tok:
ca_passwd = pin.strip()
break
return ca_passwd
@registry
class IPACertmongerExpirationCheck(IPAPlugin):
"""
Collect the known/tracked certificates and check the validity
This verifies only the information that certmonger has and uses
to schedule renewal.
This is to ensure something hasn't changed certmonger's view of
the world.
"""
@duration
def check(self):
cm = certmonger._certmonger()
all_requests = cm.obj_if.get_requests()
for req in all_requests:
request = certmonger._cm_dbus_object(cm.bus, cm, req,
certmonger.DBUS_CM_REQUEST_IF,
certmonger.DBUS_CM_IF, True)
id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF,
'nickname')
notafter = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF,
'not-valid-after')
if notafter == 0:
yield Result(self, constants.ERROR,
key=id,
msg='certmonger request id {key} does not have '
'a not-valid-after date, assuming it '
'has not been issued yet.')
continue
nafter = datetime.fromtimestamp(notafter, timezone.utc)
now = datetime.now(timezone.utc)
if now > nafter:
yield Result(self, constants.ERROR,
key=id,
expiration_date=generalized_time(nafter),
msg='Request id {key} expired on '
'{expiration_date}')
else:
delta = nafter - now
diff = int(delta.total_seconds() / DAY)
if diff < int(self.config.cert_expiration_days):
yield Result(self, constants.WARNING,
key=id,
expiration_date=generalized_time(nafter),
days=diff,
msg='Request id {key} expires in {days} '
'days. certmonger should renew this '
'automatically. Watch the status with '
'getcert list -i {key}.')
else:
yield Result(self, constants.SUCCESS,
key=id)
@registry
class IPACertfileExpirationCheck(IPAPlugin):
"""
Collect the known/tracked certificates and check file validity
Look into the certificate file or NSS database to check the
validity of the on-disk certificate.
This is to ensure a certificate wasn't replaced without
certmonger being notified.
"""
@duration
def check(self):
cm = certmonger._certmonger()
all_requests = cm.obj_if.get_requests()
for req in all_requests:
request = certmonger._cm_dbus_object(cm.bus, cm, req,
certmonger.DBUS_CM_REQUEST_IF,
certmonger.DBUS_CM_IF, True)
id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF,
'nickname')
store = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF,
'cert-storage')
if store == 'FILE':
certfile = str(request.prop_if.Get(
certmonger.DBUS_CM_REQUEST_IF, 'cert-file'))
try:
cert = x509.load_certificate_from_file(certfile)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
certfile=certfile,
error=str(e),
msg='Request id {key}: Unable to open cert '
'file \'{certfile}\': {error}')
continue
elif store == 'NSSDB':
nickname = str(request.prop_if.Get(
certmonger.DBUS_CM_REQUEST_IF, 'key_nickname'))
dbdir = str(request.prop_if.Get(
certmonger.DBUS_CM_REQUEST_IF, 'cert_database'))
try:
db = certdb.NSSDatabase(dbdir)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
dbdir=dbdir,
error=str(e),
msg='Request id {key}: Unable to open NSS '
'database \'{dbdir}\': {error}')
continue
try:
cert = db.get_cert(nickname)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
dbdir=dbdir,
nickname=nickname,
error=str(e),
msg='Request id {key}: Unable to retrieve '
'cert \'{nickname}\' from \'{dbdir}\': '
'{error}')
continue
else:
yield Result(self, constants.ERROR,
key=id,
store=store,
msg='Request id {key}: Unknown certmonger '
'storage type: {store}')
continue
now = datetime.utcnow()
notafter = cert.not_valid_after
if now > notafter:
yield Result(self, constants.ERROR,
key=id,
expiration_date=generalized_time(notafter),
msg='Request id {key} expired on '
'{expiration_date}')
continue
delta = notafter - now
diff = int(delta.total_seconds() / DAY)
if diff < int(self.config.cert_expiration_days):
yield Result(self, constants.WARNING,
key=id,
expiration_date=generalized_time(notafter),
days=diff,
msg='Request id {key} expires in {days} '
'days. certmonger should renew this '
'automatically. Watch the status with'
'getcert list -i {key}.')
else:
yield Result(self, constants.SUCCESS, key=id)
@registry
class IPACertTracking(IPAPlugin):
"""Compare the certificates tracked by certmonger to those that
are configured by default.
Steps:
1. Collect all expected certificates into `requests`
2. Get the ids of all the certificates that certmonger is tracking
3. Iterate over `requests` to retrieve the request id of the
expected tracking.
4. If the id is found we remove it from the ids list and move on
5. In the unlikely event that the request_id is not in the
ids list of all tracked certs report it.
6. Report on all tracked certs that IPA didn't setup itself as
potential issues.
"""
requires = ('dirsrv',)
@duration
def check(self):
requests = get_expected_requests(self.ca, self.ds, self.serverid)
cm = certmonger._certmonger()
ids = []
all_requests = cm.obj_if.get_requests()
for req in all_requests:
request = certmonger._cm_dbus_object(cm.bus, cm, req,
certmonger.DBUS_CM_REQUEST_IF,
certmonger.DBUS_CM_IF, True)
id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF,
'nickname')
ids.append(str(id))
for request in requests:
request_id = certmonger.get_request_id(request)
try:
if request_id is not None:
# Tracking found, move onto the next
ids.remove(request_id)
yield Result(self, constants.SUCCESS,
key=request_id)
continue
except ValueError as e:
# A request was found but the id isn't in the
# list from certmonger!?
yield Result(self, constants.ERROR,
key=request_id,
error=str(e),
msg='Found request id {key} but it is not tracked'
' by certmonger!?: {error}')
continue
# The criteria was not met
if request_id is None:
flatten = ', '.join("{!s}={!s}".format(key, val)
for (key, val) in request.items())
yield Result(self, constants.ERROR,
key=flatten,
msg='Expected certmonger tracking is missing for '
'{key}. Automated renewal will not happen '
'for this certificate')
continue
# Report any unknown certmonger requests as warnings
if ids:
for id in ids:
yield Result(self, constants.WARNING, key=id,
msg='certmonger tracking request {key} found and '
'is not expected on an IPA master.')
@registry
class IPACertDNSSAN(IPAPlugin):
"""Check whether a IPA-issued certificates have a SAN configured
Steps:
1. Collect all expected certificates into `requests`
2. Iterate over the list of certificates
3. If issued by IPA and a caIPAserviceCert then verify that
the host FQDN is in the list of SAN
4. If a CA is configured on this host then also verify that
ipa-ca.$DOMAIN is in the SAN.
"""
requires = ('dirsrv',)
@duration
def check(self):
fqdn = socket.getfqdn()
requests = get_expected_requests(self.ca, self.ds, self.serverid)
for request in requests:
request_id = certmonger.get_request_id(request)
if request_id is None:
# log and skip. Missed tracking is reported by IPACertTracking
flatten = ', '.join("{!s}={!s}".format(key, val)
for (key, val) in request.items())
logger.debug(
"Skipping %s since it is handled by IPACertTracking",
flatten
)
continue
ca_name = certmonger.get_request_value(request_id, 'ca-name')
if ca_name != 'IPA':
logger.debug('Skipping request %s with CA %s',
request_id, ca_name)
continue
profile = certmonger.get_request_value(request_id,
'template_profile')
if profile != 'caIPAserviceCert':
logger.debug('Skipping request %s with profile %s',
request_id, profile)
continue
certfile = None
if request.get('cert-file') is not None:
certfile = request.get('cert-file')
try:
cert = x509.load_certificate_from_file(certfile)
except Exception as e:
yield Result(self, constants.ERROR,
key=request_id,
certfile=certfile,
error=str(e),
msg='Unable to open cert file {certfile}: '
'{error}')
continue
elif request.get('cert-database') is not None:
nickname = request.get('cert-nickname')
dbdir = request.get('cert-database')
try:
db = certdb.NSSDatabase(dbdir)
except Exception as e:
yield Result(self, constants.ERROR,
key=request_id,
dbdir=dbdir,
error=str(e),
msg='Unable to open NSS database {dbdir}: '
'{error}')
continue
try:
cert = db.get_cert(nickname)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
dbdir=dbdir,
nickname=nickname,
error=str(e),
msg='Unable to retrieve certificate '
'\'{nickname}\' from {dbdir}: {error}')
continue
hostlist = [fqdn]
if self.ca.is_configured() and certfile == paths.HTTPD_CERT_FILE:
hostlist.append(f'{IPA_CA_RECORD}.{api.env.domain}')
error = False
for host in hostlist:
if host not in cert.san_a_label_dns_names:
error = True
yield Result(self, constants.ERROR,
key=request_id,
hostname=host,
san=cert.san_a_label_dns_names,
ca=ca_name,
profile=profile,
msg='Certificate request id {key} with '
'profile {profile} for CA {ca} does not '
'have a DNS SAN {san} matching name '
'{hostname}')
if not error:
yield Result(self, constants.SUCCESS,
key=request_id,
hostname=hostlist,
san=cert.san_a_label_dns_names,
ca=ca_name,
profile=profile)
@registry
class IPACertNSSTrust(IPAPlugin):
"""Compare the NSS trust for the CA certs to a known good value"""
@duration
def check(self):
expected_trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u',
}
kra = krainstance.KRAInstance(api.env.realm)
if kra.is_installed():
kra_trust = {
'transportCert cert-pki-kra': 'u,u,u',
'storageCert cert-pki-kra': 'u,u,u',
'auditSigningCert cert-pki-kra': 'u,u,Pu',
}
expected_trust.update(kra_trust)
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping NSS trust check')
return
db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR)
for nickname, _trust_flags in db.list_certs():
flags = certdb.unparse_trust_flags(_trust_flags)
if nickname.startswith('caSigningCert cert-pki-ca'):
expected = 'CTu,Cu,Cu'
else:
try:
expected = expected_trust[nickname]
except KeyError:
logger.debug(
"%s not found in %s, assuming 3rd party",
nickname,
paths.PKI_TOMCAT_ALIAS_DIR,
)
continue
try:
expected_trust.pop(nickname)
except KeyError:
pass
if flags != expected:
yield Result(
self, constants.ERROR, key=nickname,
expected=expected,
got=flags,
nickname=nickname,
dbdir=paths.PKI_TOMCAT_ALIAS_DIR,
msg='Incorrect NSS trust for {nickname} in {dbdir}. '
'Got {got} expected {expected}.')
continue
yield Result(self, constants.SUCCESS, key=nickname)
for nickname in expected_trust:
yield Result(
self, constants.ERROR,
key=nickname,
nickname=nickname,
dbdir=paths.PKI_TOMCAT_ALIAS_DIR,
msg='Certificate {nickname} missing from {dbdir} while '
'verifying trust')
@registry
class IPACertMatchCheck(IPAPlugin):
"""
Ensure certificates match between LDAP and NSS databases
"""
requires = ('dirsrv',)
def get_cert_list_from_db(self, nssdb, nickname):
"""
Retrieve all certificates from an NSS database for nickname.
"""
try:
args = ["-L", "-n", nickname, "-a"]
result = nssdb.run_certutil(args, capture_output=True)
return x509.load_certificate_list(result.raw_output)
except ipautil.CalledProcessError:
return []
@duration
def check(self):
if not self.ca.is_configured():
logger.debug("No CA configured, skipping certificate match check")
return
# Ensure /etc/ipa/ca.crt matches the NSS DB CA certificates
def match_cacert_and_db(plugin, cacerts, dbpath):
db = certs.CertDB(api.env.realm, dbpath)
nickname = '%s IPA CA' % api.env.realm
try:
dbcacerts = self.get_cert_list_from_db(db, nickname)
except Exception as e:
yield Result(plugin, constants.ERROR,
key=nickname,
error=str(e),
msg='Unable to load CA cert: {error}')
return False
ok = True
for cert in dbcacerts:
if cert not in cacerts:
ok = False
yield Result(plugin, constants.ERROR,
key=nickname,
nickname=nickname,
serial_number=cert.serial_number,
dbdir=dbpath,
certdir=paths.IPA_CA_CRT,
msg=('CA Certificate nickname {nickname} '
'with serial number {serial} '
'is in {dbdir} but is not in'
'%s' % paths.IPA_CA_CRT))
return ok
try:
cacerts = x509.load_certificate_list_from_file(paths.IPA_CA_CRT)
except Exception:
yield Result(self, constants.ERROR,
key=paths.IPA_CA_CRT.replace(os.path.sep, '_'),
path=paths.IPA_CA_CRT,
msg='Unable to load CA cert file {path}: {error}')
return
# Ensure CA cert entry from LDAP matches /etc/ipa/ca.crt
dn = DN('cn=%s IPA CA' % api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
api.env.basedn)
try:
entry = self.conn.get_entry(dn)
except errors.NotFound:
yield Result(self, constants.ERROR,
key=str(dn),
dn=str(dn),
msg='CA Certificate entry \'{dn}\' '
'not found in LDAP')
return
cacerts_ok = True
# Are all the certs in LDAP for the IPA CA in /etc/ipa/ca.crt
for cert in entry['CACertificate']:
if cert not in cacerts:
cacerts_ok = False
yield Result(self, constants.ERROR,
key=str(dn),
dn=str(dn),
serial_number=cert.serial_number,
msg=('CA Certificate serial number {serial} is '
'in LDAP \'{dn}\' but is not in '
'%s' % paths.IPA_CA_CRT))
# Ensure NSS DBs have matching CA certs for /etc/ipa/ca.crt
serverid = realm_to_serverid(api.env.realm)
dspath = paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % serverid
cacertds_ok = yield from match_cacert_and_db(self, cacerts, dspath)
cacertnss_ok = yield from match_cacert_and_db(self, cacerts,
paths.IPA_NSSDB_DIR)
if cacerts_ok:
yield Result(self, constants.SUCCESS,
key=paths.IPA_CA_CRT)
if cacertds_ok:
yield Result(self, constants.SUCCESS,
key=dspath)
if cacertnss_ok:
yield Result(self, constants.SUCCESS,
key=paths.IPA_NSSDB_DIR)
@registry
class IPADogtagCertsMatchCheck(IPAPlugin):
"""
Check if dogtag certs present in both NSS DB and LDAP match
"""
requires = ('dirsrv',)
@duration
def check(self):
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping connectivity check')
return
def match_ldap_nss_cert(plugin, ldap, db, cert_dn, attr, cert_nick):
try:
entry = ldap.get_entry(cert_dn)
except errors.NotFound:
yield Result(plugin, constants.ERROR,
key=cert_dn,
msg='%s entry not found in LDAP' % cert_dn)
return False
try:
nsscert = db.get_cert_from_db(cert_nick)
except Exception as e:
yield Result(plugin, constants.ERROR,
key=cert_nick,
error=str(e),
msg=('Unable to load %s certificate:'
'{error}' % cert_nick))
return False
cert_matched = any(cert == nsscert for cert in entry[attr])
if not cert_matched:
yield Result(plugin, constants.ERROR,
key=cert_nick,
nickname=cert_nick,
dbdir=db.secdir,
msg=('{nickname} certificate in NSS DB {dbdir} '
'does not match entry in LDAP'))
return False
return True
def match_ldap_nss_certs_by_subject(plugin, ldap, db, dn,
expected_nicks_subjects):
entries = ldap.get_entries(dn)
all_ok = True
for nick, subject in expected_nicks_subjects.items():
cert = db.get_cert_from_db(nick)
ok = any(
cert in entry["userCertificate"]
and subject == entry["subjectName"][0]
for entry in entries
if "userCertificate" in entry
)
if not ok:
all_ok = False
yield Result(plugin, constants.ERROR,
key=nick,
nickname=nick,
dbdir=db.secdir,
msg=('{nickname} certificate in NSS DB '
'{dbdir} does not match entry in LDAP'))
return all_ok
db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR)
dn = DN('uid=pkidbuser,ou=people,o=ipaca')
subsystem_nick = 'subsystemCert cert-pki-ca'
subsystem_ok = yield from match_ldap_nss_cert(self, self.conn,
db, dn,
'userCertificate',
subsystem_nick)
dn = DN('cn=%s IPA CA' % api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
api.env.basedn)
casigning_nick = 'caSigningCert cert-pki-ca'
casigning_ok = yield from match_ldap_nss_cert(self, self.conn,
db, dn, 'CACertificate',
casigning_nick)
expected_nicks_subjects = {
'ocspSigningCert cert-pki-ca':
'CN=OCSP Subsystem,O=%s' % api.env.realm,
'subsystemCert cert-pki-ca':
'CN=CA Subsystem,O=%s' % api.env.realm,
'auditSigningCert cert-pki-ca':
'CN=CA Audit,O=%s' % api.env.realm,
'Server-Cert cert-pki-ca':
'CN=%s,O=%s' % (api.env.host, api.env.realm),
}
kra = krainstance.KRAInstance(api.env.realm)
if kra.is_installed():
kra_expected_nicks_subjects = {
'transportCert cert-pki-kra':
'CN=KRA Transport Certificate,O=%s' % api.env.realm,
'storageCert cert-pki-kra':
'CN=KRA Storage Certificate,O=%s' % api.env.realm,
'auditSigningCert cert-pki-kra':
'CN=KRA Audit,O=%s' % api.env.realm,
}
expected_nicks_subjects.update(kra_expected_nicks_subjects)
ipaca_basedn = DN('ou=certificateRepository,ou=ca,o=ipaca')
ipaca_certs_ok = yield from match_ldap_nss_certs_by_subject(
self, self.conn, db,
ipaca_basedn,
expected_nicks_subjects
)
if subsystem_ok:
yield Result(self, constants.SUCCESS,
key=subsystem_nick)
if casigning_ok:
yield Result(self, constants.SUCCESS,
key=casigning_nick)
if ipaca_certs_ok:
yield Result(self, constants.SUCCESS,
key=str(ipaca_basedn))
@registry
class IPANSSChainValidation(IPAPlugin):
"""Validate the certificate chain of the certs."""
def validate_nss(self, dbdir, dbtype, pinfile, nickname):
"""Call out to certutil to verify a certificate.
The caller must handle the exceptions
"""
args = [paths.CERTUTIL, '-V', '-u', 'V', '-e']
args.extend(['-d', dbtype + ':' + dbdir])
args.extend(['-n', nickname])
args.extend(['-f', pinfile])
return ipautil.run(args, raiseonerr=False)
@duration
def check(self):
validate = []
ca_pw_fname = None
if self.ca.is_configured():
try:
ca_passwd = get_dogtag_cert_password()
except IOError as e:
yield Result(
self, constants.ERROR,
key='db_authenticate',
error=str(e),
msg='Unable to read CA NSSDB token password: {error}')
return
else:
with tempfile.NamedTemporaryFile(mode='w',
delete=False) as ca_pw_file:
ca_pw_file.write(ca_passwd)
ca_pw_fname = ca_pw_file.name
validate.append(
(
paths.PKI_TOMCAT_ALIAS_DIR,
'Server-Cert cert-pki-ca',
ca_pw_fname,
),
)
validate.append(
(
dsinstance.config_dirname(self.serverid),
self.ds.get_server_cert_nickname(self.serverid),
os.path.join(dsinstance.config_dirname(self.serverid),
'pwdfile.txt'),
)
)
# Wrap in try/except to ensure the temporary password file is
# removed
try:
for (dbdir, nickname, pinfile) in validate:
# detect the database type so we have the right prefix
db = certdb.NSSDatabase(dbdir)
key = os.path.normpath(dbdir) + ':' + nickname
try:
response = self.validate_nss(dbdir, db.dbtype, pinfile,
nickname)
except ipautil.CalledProcessError as e:
logger.debug('Validation of NSS certificate failed %s', e)
yield Result(
self, constants.ERROR,
key=key,
dbdir=dbdir,
nickname=nickname,
reason=response.output_error,
msg='Validation of {nickname} in {dbdir} failed: '
'{reason}')
else:
if 'certificate is valid' not in \
response.raw_output.decode('utf-8'):
yield Result(
self, constants.ERROR,
key=key,
dbdir=dbdir,
nickname=nickname,
reason="%s: %s" %
(response.raw_output.decode('utf-8'),
response.error_log),
msg='Validation of {nickname} in {dbdir} failed: '
'{reason}')
else:
yield Result(self, constants.SUCCESS,
dbdir=dbdir, nickname=nickname,
key=key)
finally:
if ca_pw_fname:
ipautil.remove_file(ca_pw_fname)
@registry
class IPAOpenSSLChainValidation(IPAPlugin):
"""Validate the certificate chain of the certs."""
def validate_openssl(self, file):
"""Call out to openssl to verify a certificate against global chain
The caller must handle the exceptions
"""
args = [paths.OPENSSL, 'verify',
'-verbose',
'-show_chain',
'-CAfile', paths.IPA_CA_CRT,
file]
return ipautil.run(args, raiseonerr=False)
@duration
def check(self):
certlist = [paths.HTTPD_CERT_FILE]
if self.ca.is_configured():
certlist.append(paths.RA_AGENT_PEM)
for cert in certlist:
try:
response = self.validate_openssl(cert)
except Exception as e:
yield Result(
self, constants.ERROR,
key=cert,
error=str(e),
msg='Certificate validation for {key} failed: {error}')
continue
else:
if ': OK' not in response.raw_output.decode('utf-8'):
yield Result(
self, constants.ERROR, key=cert,
reason=response.raw_error_output.decode('utf-8'),
msg='Certificate validation for {key} failed: '
'{reason}')
else:
yield Result(
self, constants.SUCCESS, key=cert)
def check_agent(plugin, base_dn, agent_type):
"""Check RA/KRA Agent"""
try:
cert = x509.load_certificate_from_file(paths.RA_AGENT_PEM)
except Exception as e:
yield Result(plugin, constants.ERROR,
key=paths.RA_AGENT_PEM.replace(os.path.sep, '_'),
error=str(e),
msg='Unable to load RA cert: {error}')
return
serial_number = cert.serial_number
subject = DN(cert.subject)
issuer = DN(cert.issuer)
description = '2;%d;%s;%s' % (serial_number, issuer, subject)
logger.debug('%s agent description should be %s', agent_type, description)
db_filter = ldap2.ldap2.combine_filters(
[
ldap2.ldap2.make_filter({'objectClass': 'inetOrgPerson'}),
ldap2.ldap2.make_filter(
{'description': ';%s;%s' % (issuer, subject)},
exact=False, trailing_wildcard=False),
],
ldap2.ldap2.MATCH_ALL)
try:
entries = plugin.conn.get_entries(base_dn,
plugin.conn.SCOPE_SUBTREE,
db_filter)
except errors.NotFound:
yield Result(plugin, constants.ERROR,
key=agent_type,
description=description,
msg='%s agent not found in LDAP' % agent_type)
return
except Exception as e:
yield Result(plugin, constants.ERROR,
key=agent_type,
error=str(e),
msg='Retrieving %s agent from LDAP failed {error}'
% agent_type)
return
else:
logger.debug('%s agent description is %s', agent_type, description)
if len(entries) != 1:
yield Result(plugin, constants.ERROR,
key='too_many_agents',
found=len(entries),
msg='Too many %s agent entries found, {found}'
% agent_type)
return
entry = entries[0]
raw_desc = entry.get('description')
if raw_desc is None:
yield Result(plugin, constants.ERROR,
key='agent_missing_description',
msg='%s agent is missing the description '
'attribute or it is not readable' % agent_type)
return
ra_desc = raw_desc[0]
ra_certs = entry.get('usercertificate')
if ra_desc != description:
yield Result(plugin, constants.ERROR,
key='description_mismatch',
expected=description,
got=ra_desc,
msg='%s agent description does not match. Found '
'{got} in LDAP and expected {expected}' % agent_type)
return
found = False
for candidate in ra_certs:
if candidate == cert:
found = True
break
if not found:
yield Result(plugin, constants.ERROR,
key='ldap_mismatch',
certfile=paths.RA_AGENT_PEM,
dn=str(entry.dn),
msg='%s agent certificate in {certfile} not '
'found in LDAP userCertificate attribute '
'for the entry {dn}' % agent_type)
yield Result(plugin, constants.SUCCESS)
@registry
class IPARAAgent(IPAPlugin):
"""Validate the RA Agent used to talk to the CA
Compare the description and usercertificate values.
"""
requires = ('dirsrv',)
@duration
def check(self):
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping RA Agent check')
return
base_dn = DN('uid=ipara,ou=people,o=ipaca')
yield from check_agent(self, base_dn, 'RA')
@registry
class IPAKRAAgent(IPAPlugin):
"""Validate the KRA Agent
Compare the description and usercertificate values.
"""
requires = ('dirsrv',)
@duration
def check(self):
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping KRA Agent check')
return
kra = krainstance.KRAInstance(api.env.realm)
if not kra.is_installed():
logger.debug('KRA is not installed, skipping KRA Agent check')
return
base_dn = DN('uid=ipakra,ou=people,o=kra,o=ipaca')
yield from check_agent(self, base_dn, 'KRA')
@registry
class IPACertRevocation(IPAPlugin):
"""Confirm that the IPA certificates are not revoked
This uses the certmonger expected tracking list to know which
one(s) to consider.
"""
revocation_reason = [
"unspecified",
"key compromise",
"CA compromise",
"affiliation changed",
"superseded",
"cessation of operation",
"certificate hold",
"", # unused
"remove from CRL",
"privilege withdrawn",
"AA compromise",
]
requires = ('dirsrv',)
@duration
def check(self):
# For simplicity use the expected certmonger tracking for the
# list of certificates to check because it already filters out
# based on whether the CA system is configure and whether the
# certificates were issued by IPA.
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping revocation check')
return
requests = get_expected_requests(self.ca, self.ds, self.serverid)
for request in requests:
id = certmonger.get_request_id(request)
if request.get('cert-file') is not None:
certfile = request.get('cert-file')
try:
cert = x509.load_certificate_from_file(certfile)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
certfile=certfile,
error=str(e),
msg='Unable to open cert file {certfile}: '
'{error}')
continue
elif request.get('cert-database') is not None:
nickname = request.get('cert-nickname')
dbdir = request.get('cert-database')
try:
db = certdb.NSSDatabase(dbdir)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
dbdir=dbdir,
error=str(e),
msg='Unable to open NSS database {dbdir}: '
'{error}')
continue
try:
cert = db.get_cert(nickname)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
dbdir=dbdir,
nickname=nickname,
error=str(e),
msg='Unable to retrieve certificate '
'\'{nickname}\' from {dbdir}: {error}')
continue
else:
yield Result(self, constants.ERROR,
key=id,
msg='Unable to to identify certificate storage '
'type for request {key}')
continue
issued = is_ipa_issued_cert(api, cert)
if issued is False:
logger.debug('\'%s\' was not issued by IPA, skipping',
DN(cert.subject))
continue
if issued is None:
logger.debug('LDAP is down, skipping \'%s\'',
DN(cert.subject))
continue
# Now we have the cert either way, check the recovation
try:
result = api.Command.cert_show(cert.serial_number,
all=True)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
serial=cert.serial_number,
error=str(e),
msg='Request for certificate serial number '
'{serial} in request {key} failed: {error}')
continue
try:
if result['result']['revoked']:
reason = result['result']['revocation_reason']
reason_txt = self.revocation_reason[reason]
yield Result(self, constants.ERROR,
revocation_reason=reason_txt,
key=id,
msg='Certificate tracked by {key} is revoked '
'{revocation_reason}')
else:
yield Result(self, constants.SUCCESS, key=id)
except Exception as e:
yield Result(self, constants.ERROR,
key=id,
error=str(e),
msg='Unable to determine revocation '
'status for {key}: {error}')
@registry
class IPACertmongerCA(IPAPlugin):
"""Ensure that the required CAs are available in certmonger
Addresses symptom of https://pagure.io/freeipa/issue/7870
"""
def find_ca(self, name):
cm = certmonger._certmonger()
ca_path = cm.obj_if.find_ca_by_nickname(name)
return certmonger._cm_dbus_object(cm.bus, cm, ca_path,
certmonger.DBUS_CM_CA_IF,
certmonger.DBUS_CM_IF, True)
@duration
def check(self):
ca_list = ['IPA']
if self.ca.is_configured():
ca_list.extend([
'dogtag-ipa-ca-renew-agent',
'dogtag-ipa-ca-renew-agent-reuse'
])
for ca in ca_list:
logger.debug('Checking for existence of certmonger CA \'%s\'',
ca)
try:
self.find_ca(ca)
except Exception as e:
logger.debug('Search for certmonger CA %s failed: %s', ca, e)
yield Result(self, constants.ERROR,
key=ca,
msg='Certmonger CA \'{key}\' missing')
else:
yield Result(self, constants.SUCCESS,
key=ca)
@registry
class IPACAChainExpirationCheck(IPAPlugin):
"""Verify that the certs in the CA chain in /etc/ipa/ca.crt are valid
"""
@duration
def check(self):
try:
ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT)
except IOError as e:
logger.debug("Could not open %s: %s", paths.IPA_CA_CRT, e)
yield Result(self, constants.ERROR,
key=paths.IPA_CA_CRT,
error=str(e),
msg='Error opening IPA CA chain at {key}: {error}')
return
except ValueError as e:
logger.debug(
"% contains an invalid certificate", paths.IPA_CA_CRT
)
yield Result(self, constants.ERROR,
key=paths.IPA_CA_CRT,
error=str(e),
msg='IPA CA chain {key} contains an invalid '
'certificate: {error}')
return
now = datetime.now(timezone.utc)
soon = now + timedelta(days=int(self.config.cert_expiration_days))
for cert in ca_certs:
subject = DN(cert.subject)
subject = str(subject).replace('\\;', '\\3b')
dt = cert.not_valid_after.replace(tzinfo=timezone.utc)
if dt < now:
logger.debug("%s is expired", subject)
yield Result(self, constants.CRITICAL,
path=paths.IPA_CA_CRT,
key=subject,
msg='CA \'{key}\' in {path} is expired.')
elif dt <= soon:
logger.debug("%s is expiring soon", subject)
yield Result(self, constants.WARNING,
path=paths.IPA_CA_CRT,
key=subject,
days=(dt - now).days,
msg='CA \'{key}\' in {path} is expiring in '
'{days} days.')
else:
yield Result(self, constants.SUCCESS,
path=paths.IPA_CA_CRT,
key=subject,
days=(dt - now).days)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/dna.py 0000664 0000000 0000000 00000003235 14200534377 0022727 0 ustar 00root root 0000000 0000000
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
from ipalib import api
from ipaserver.install import replication
logger = logging.getLogger()
@registry
class IPADNARangeCheck(IPAPlugin):
"""
Report the configured DNA range, if any.
This expects some external system to analyze and determine if
any or all masters have a DNA range configured. It is not an error
if a master does not have a range. It IS an error if no masters have
a range.
"""
requires = ('dirsrv',)
@duration
def check(self):
agmt = replication.ReplicationManager(api.env.realm, api.env.host)
(range_start, range_max) = agmt.get_DNA_range(api.env.host)
(next_start, next_max) = agmt.get_DNA_next_range(api.env.host)
if range_start is not None:
yield Result(self, constants.SUCCESS,
range_start=range_start,
range_max=range_max,
next_start=next_start or 0,
next_max=next_max or 0)
else:
yield Result(self, constants.WARNING,
key='no_dna_range_defined',
range_start=0,
range_max=0,
next_start=0,
next_max=0,
msg='No DNA range defined. If no masters define a '
'range then users and groups cannot be '
'created.')
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/files.py 0000664 0000000 0000000 00000015047 14200534377 0023273 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import glob
import logging
import os
from ipahealthcheck.core.files import FileCheck
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipalib import api, errors
from ipaplatform.paths import paths
from ipaplatform.constants import constants
from ipapython.certdb import NSS_SQL_FILES
from ipapython.dn import DN
from ipaserver.install import dsinstance
from ipaserver.install import krbinstance
logger = logging.getLogger()
@registry
class IPAFileNSSDBCheck(IPAPlugin, FileCheck):
def collect_files(self, basedir, filelist, owner, group, perms):
for file in filelist:
self.files.append((os.path.join(basedir, file), owner, group,
perms))
def check(self):
self.files = []
self.collect_files(dsinstance.config_dirname(self.serverid),
NSS_SQL_FILES, 'dirsrv', 'root', '0640')
# There always has to be a special one. pkcs11.txt has a different
# group so pop off the auto-generated one and add a replacement.
old = (os.path.join(dsinstance.config_dirname(self.serverid),
'pkcs11.txt'), 'dirsrv', 'root', '0640')
self.files.remove(old)
new = (os.path.join(dsinstance.config_dirname(self.serverid),
'pkcs11.txt'), 'dirsrv', 'dirsrv', '0640')
self.files.append(new)
if self.ca.is_configured():
self.collect_files(paths.PKI_TOMCAT_ALIAS_DIR, NSS_SQL_FILES,
'pkiuser', 'pkiuser', '0600')
return FileCheck.check(self)
@registry
class IPAFileCheck(IPAPlugin, FileCheck):
def dns_container_exists(self):
try:
self.conn.get_entry(DN(api.env.container_dns,
api.env.basedn), [])
except errors.NotFound:
return False
except AttributeError:
logger.debug("LDAP is down, can't tell whether DNS is available."
" Skipping those file checks.")
return False
return True
def check(self):
self.files = []
if self.ca.is_configured():
self.files.append((paths.RA_AGENT_PEM, 'root', 'ipaapi', '0440'))
self.files.append((paths.RA_AGENT_KEY, 'root', 'ipaapi', '0440'))
if krbinstance.is_pkinit_enabled():
self.files.append((paths.KDC_CERT, 'root', 'root', '0644'))
self.files.append((paths.KDC_KEY, 'root', 'root', '0600'))
if self.dns_container_exists():
self.files.append((paths.NAMED_KEYTAB,
constants.NAMED_USER,
constants.NAMED_GROUP, '0400'))
if os.path.exists(paths.IPA_DNSKEYSYNCD_KEYTAB):
self.files.append((paths.IPA_DNSKEYSYNCD_KEYTAB,
'root', constants.ODS_GROUP, '0440'))
self.files.append((paths.GSSAPI_SESSION_KEY,
'root', 'root', '0600'))
self.files.append((paths.DS_KEYTAB,
constants.DS_USER, constants.DS_GROUP, '0600'))
self.files.append((paths.IPA_CA_CRT, 'root', 'root', '0644'))
self.files.append((paths.IPA_CUSTODIA_KEYS, 'root', 'root', '0600'))
self.files.append((paths.RESOLV_CONF, ('root', 'systemd-resolve'),
('root', 'systemd-resolve'), '0644'))
self.files.append((paths.HOSTS, 'root', 'root', '0644'))
# IPA log files that may vary by installation. Only verify
# those that exist
for filename in (
paths.IPABACKUP_LOG,
paths.IPARESTORE_LOG,
paths.IPACLIENT_INSTALL_LOG,
paths.IPACLIENT_UNINSTALL_LOG,
paths.IPAREPLICA_CA_INSTALL_LOG,
paths.IPAREPLICA_CONNCHECK_LOG,
paths.IPAREPLICA_INSTALL_LOG,
paths.IPASERVER_INSTALL_LOG,
paths.IPASERVER_KRA_INSTALL_LOG,
paths.IPASERVER_UNINSTALL_LOG,
paths.IPAUPGRADE_LOG,
paths.IPATRUSTENABLEAGENT_LOG,
):
if os.path.exists(filename):
self.files.append((filename, 'root', 'root', '0600'))
self.files.append((paths.IPA_CUSTODIA_AUDIT_LOG,
'root', 'root', '0644'))
self.files.append((paths.KADMIND_LOG, 'root', 'root', '0600'))
self.files.append((paths.KRB5KDC_LOG, 'root', 'root', '0640'))
inst = api.env.realm.replace('.', '-')
self.files.append((paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst,
'dirsrv', 'dirsrv', '0600'))
self.files.append((paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst,
'dirsrv', 'dirsrv', '0600'))
self.files.append((paths.VAR_LOG_HTTPD_ERROR, 'root', 'root', '0644'))
for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_CA_DIR):
self.files.append((globpath, "pkiuser", "pkiuser", "0644"))
for globpath in glob.glob(
"%s/ca_audit*" % paths.TOMCAT_SIGNEDAUDIT_DIR
):
self.files.append((globpath, 'pkiuser', 'pkiuser', '0640'))
for filename in ('selftests.log', 'system', 'transactions'):
self.files.append((
os.path.join(paths.TOMCAT_CA_DIR, filename),
'pkiuser', 'pkiuser', '0640'
))
for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_KRA_DIR):
self.files.append((globpath, "pkiuser", "pkiuser", "0644"))
for globpath in glob.glob(
"%s/ca_audit*" % paths.TOMCAT_KRA_SIGNEDAUDIT_DIR
):
self.files.append((globpath, 'pkiuser', 'pkiuser', '0640'))
for filename in ('selftests.log', 'system', 'transactions'):
self.files.append((
os.path.join(paths.TOMCAT_KRA_DIR, filename),
'pkiuser', 'pkiuser', '0640'
))
return FileCheck.check(self)
@registry
class TomcatFileCheck(IPAPlugin, FileCheck):
def check(self):
if not self.ca.is_configured():
logger.debug('CA is not configured, skipping')
self.files = []
else:
self.files = [
(paths.PKI_TOMCAT_PASSWORD_CONF,
constants.PKI_USER, constants.PKI_GROUP, '0660'),
(paths.CA_CS_CFG_PATH,
constants.PKI_USER, constants.PKI_GROUP, '0660'),
(os.path.join(paths.PKI_TOMCAT, 'server.xml'),
constants.PKI_USER, constants.PKI_GROUP, '0660'),
]
return FileCheck.check(self)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/host.py 0000664 0000000 0000000 00000002212 14200534377 0023134 0 ustar 00root root 0000000 0000000
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import gssapi
import logging
import os
import tempfile
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
from ipalib import api
from ipalib.install.kinit import kinit_keytab
from ipaplatform.paths import paths
from ipapython import ipautil
logger = logging.getLogger()
@registry
class IPAHostKeytab(IPAPlugin):
"""Ensure the host keytab can get a TGT"""
requires = ('krb5kdc', 'dirsrv')
@duration
def check(self):
ccache_dir = tempfile.mkdtemp()
ccache_name = os.path.join(ccache_dir, 'ccache')
try:
try:
host_princ = str('host/%s@%s' % (api.env.host, api.env.realm))
kinit_keytab(host_princ, paths.KRB5_KEYTAB, ccache_name)
except gssapi.exceptions.GSSError as e:
yield Result(self, constants.ERROR,
msg='Failed to obtain host TGT: %s' % e)
finally:
ipautil.remove_file(ccache_name)
os.rmdir(ccache_dir)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/idns.py 0000664 0000000 0000000 00000022322 14200534377 0023120 0 ustar 00root root 0000000 0000000
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from dns import rdatatype
from dns.exception import DNSException
import logging
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
from ipalib import api
try:
from dns.resolver import resolve
except ImportError:
from dns.resolver import query as resolve
logger = logging.getLogger()
def query_uri(uri):
try:
answers = resolve(uri, rdatatype.URI)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
return answers
@registry
class IPADNSSystemRecordsCheck(IPAPlugin):
"""
Verify that the expected DNS service records are resolvable.
IPA will already provide the values we need to validate with the
IPASystemRecords class. We just need to pull that and do the
equivalent DNS lookups.
"""
requires = ('dirsrv',)
def srv_to_name(self, srv, target):
"""Combine the SRV record and target into a unique name."""
return srv + ":" + target
def uri_to_name(self, uri, target):
"""Combine the SRV record and target into a unique name."""
return uri + ":" + target
@duration
def check(self):
# pylint: disable=import-outside-toplevel
from ipapython.dnsutil import query_srv
from ipaserver.dns_data_management import IPASystemRecords
# pylint: enable=import-outside-toplevel
system_records = IPASystemRecords(api)
base_records = system_records.get_base_records()
# collect the list of expected values
txt_rec = dict()
srv_rec = dict()
uri_rec = dict()
a_rec = list()
aaaa_rec = list()
for name, node in base_records.items():
for rdataset in node:
for rd in rdataset:
if rd.rdtype == rdatatype.SRV:
if name.ToASCII() in srv_rec:
srv_rec[name.ToASCII()].append(rd.target.to_text())
else:
srv_rec[name.ToASCII()] = [rd.target.to_text()]
elif rd.rdtype == rdatatype.TXT:
if name.ToASCII() in txt_rec:
txt_rec[name.ToASCII()].append(rd.to_text())
else:
txt_rec[name.ToASCII()] = [rd.to_text()]
elif rd.rdtype == rdatatype.A:
a_rec.append(rd.to_text())
elif rd.rdtype == rdatatype.AAAA:
aaaa_rec.append(rd.to_text())
elif rd.rdtype == rdatatype.URI:
if name.ToASCII() in uri_rec:
uri_rec[name.ToASCII()].append(
rd.target.decode('utf-8')
)
else:
uri_rec[name.ToASCII()] = [
rd.target.decode('utf-8')
]
else:
logger.error("Unhandled rdtype %d", rd.rdtype)
# For each SRV record that IPA thinks it should have, do a DNS
# lookup of it and ensure that DNS has the same set of values
# that IPA thinks it should.
for srv in srv_rec:
logger.debug("Search DNS for SRV record of %s", srv)
try:
answers = query_srv(srv)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
hosts = srv_rec[srv]
for answer in answers:
logger.debug("DNS record found: %s", answer)
try:
hosts.remove(answer.target.to_text())
yield Result(
self, constants.SUCCESS,
key=self.srv_to_name(srv, answer.target.to_text()))
except ValueError:
yield Result(
self, constants.WARNING,
msg='Unexpected SRV entry in DNS',
key=self.srv_to_name(srv, answer.target.to_text()))
for host in hosts:
yield Result(
self, constants.WARNING,
msg='Expected SRV record missing',
key=self.srv_to_name(srv, host))
for uri in uri_rec:
logger.debug("Search DNS for URI record of %s", uri)
answers = query_uri(uri)
hosts = uri_rec[uri]
for answer in answers:
logger.debug("DNS record found: %s", answer)
try:
hosts.remove(answer.target.decode('utf-8'))
yield Result(
self, constants.SUCCESS,
key=self.uri_to_name(
uri, answer.target.decode('utf-8')
)
)
except ValueError:
yield Result(
self, constants.WARNING,
msg='Unexpected URI entry in DNS',
key=self.uri_to_name(
uri, answer.target.decode('utf-8')
)
)
for host in hosts:
yield Result(
self, constants.WARNING,
msg='Expected URI record missing',
key=self.uri_to_name(uri, host)
)
for txt in txt_rec:
logger.debug("Search DNS for TXT record of %s", txt)
try:
answers = resolve(txt, rdatatype.TXT)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
realms = txt_rec[txt]
for answer in answers:
logger.debug("DNS record found: %s", answer)
realm = answer.to_text()
try:
realms.remove(realm)
yield Result(self, constants.SUCCESS,
key=realm)
except ValueError:
yield Result(self, constants.WARNING,
key=realm,
msg='expected realm missing')
if a_rec:
# Look up the ipa-ca records
qname = "ipa-ca." + api.env.domain + "."
logger.debug("Search DNS for A record of %s", qname)
try:
answers = resolve(qname, rdatatype.A)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
for answer in answers:
logger.debug("DNS record found: %s", answer)
ipaddr = answer.to_text()
try:
yield Result(self, constants.SUCCESS,
key=ipaddr)
except ValueError:
yield Result(self, constants.WARNING,
key=ipaddr,
msg='expected ipa-ca IPv4 address missing')
ca_count = 0
for server in system_records.servers_data:
master = system_records.servers_data.get(server)
if 'CA server' in master.get('roles'):
ca_count += 1
if len(answers) != ca_count:
yield Result(
self, constants.WARNING,
key='ca_count_a_rec',
msg='Got {count} ipa-ca A records, expected {expected}',
count=len(answers),
expected=ca_count)
if aaaa_rec:
# Look up the ipa-ca records
qname = "ipa-ca." + api.env.domain + "."
logger.debug("Search DNS for AAAA record of %s", qname)
try:
answers = resolve(qname, rdatatype.AAAA)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
for answer in answers:
logger.debug("DNS record found: %s", answer)
ipaddr = answer.to_text()
try:
yield Result(self, constants.SUCCESS,
key=ipaddr)
except ValueError:
yield Result(self, constants.WARNING,
key=ipaddr,
msg='expected ipa-ca IPv6 address missing')
ca_count = 0
for server in system_records.servers_data:
master = system_records.servers_data.get(server)
if 'CA server' in master.get('roles'):
ca_count += 1
if len(answers) != ca_count:
yield Result(
self, constants.WARNING,
key='ca_count_aaaa_rec',
msg='Got {count} ipa-ca AAAA records, expected {expected}',
count=len(answers),
expected=ca_count)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/meta.py 0000664 0000000 0000000 00000001513 14200534377 0023110 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipalib import api
@registry
class IPAMetaCheck(IPAPlugin):
"""Return meta data for the IPA installation"""
requires = ('dirsrv',)
@duration
def check(self):
try:
result = api.Command.server_find(pkey_only=True)
except Exception as e:
yield Result(self, constants.ERROR,
msg='server-show failed, %s' % e)
else:
masters = []
for server in result['result']:
masters.append(server['cn'][0])
yield Result(self, constants.SUCCESS,
masters=masters)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/nss.py 0000664 0000000 0000000 00000003502 14200534377 0022765 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
import grp
import logging
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
logger = logging.getLogger()
# A tuple of groups and a tuple of expected members
#
# For example the apache user needs to be in the ipaapi group so
# the tuple would look like: 'ipaapi', ('apache',).
#
# The second value is a tuple so that we can more easily extend if
# multiple users need to be a member of a group.
#
# (group_name, (members,))
GROUP_MEMBERS = (
('ipaapi', ('apache',)),
)
@registry
class IPAGroupMemberCheck(IPAPlugin):
"""
Ensure that nss/POSIX group membership is as expected.
This can be critical for security and/or proper access control and
is primarily being checked for privilege separation. The ipaapi
user needs to be able to read ccaches created by Apache.
"""
@duration
def check(self):
for (group, members) in GROUP_MEMBERS:
try:
grp_group = grp.getgrnam(group)
except KeyError:
yield Result(self, constants.ERROR,
key=group,
msg='group {key} does not exist')
continue
for member in members:
if member not in grp_group.gr_mem:
yield Result(self, constants.ERROR,
key=group,
member=member,
msg='{member} is not a member of group {key}')
else:
yield Result(self, constants.SUCCESS,
key=group,
member=member)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/plugin.py 0000664 0000000 0000000 00000006100 14200534377 0023455 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipalib import api, errors
try:
from ipapython.ipaldap import realm_to_serverid
except ImportError:
from ipaserver.install.installutils import realm_to_serverid
from ipaserver.install import cainstance
from ipaserver.install import dsinstance
from ipaserver.install import httpinstance
from ipaserver.install import installutils
from ipahealthcheck.core.plugin import Plugin, Registry
logger = logging.getLogger()
class IPAPlugin(Plugin):
def __init__(self, reg):
super().__init__(reg)
self.ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host)
self.http = httpinstance.HTTPInstance()
self.ds = dsinstance.DsInstance()
self.serverid = realm_to_serverid(api.env.realm)
self.conn = api.Backend.ldap2
class IPARegistry(Registry):
def __init__(self):
super().__init__()
self.trust_agent = False
self.trust_controller = False
self.ca_configured = False
def initialize(self, framework, config, options=None):
super().initialize(framework, config)
# deferred import for mock
# pylint: disable=import-outside-toplevel
from ipaserver.servroles import ADtrustBasedRole, ServiceBasedRole
# pylint: enable=import-outside-toplevel
installutils.check_server_configuration()
if not api.isdone('finalize'):
if not api.isdone('bootstrap'):
api.bootstrap(in_server=True,
context='ipahealthcheck',
log=None)
if not api.isdone('finalize'):
api.finalize()
if not api.Backend.ldap2.isconnected():
try:
api.Backend.ldap2.connect()
except (errors.CCacheError, errors.NetworkError) as e:
logger.debug('Failed to connect to LDAP: %s', e)
return
ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host)
self.ca_configured = ca.is_configured()
# This package is pulled in when the trust package is installed
# and is required to lookup trust users. If this is not installed
# then it can be inferred that trust is not enabled.
try:
# pylint: disable=unused-import,import-outside-toplevel
import pysss_nss_idmap # noqa: F401
# pylint: enable=unused-import,import-outside-toplevel
except ImportError:
return
roles = (
ADtrustBasedRole(u"ad_trust_agent_server",
u"AD trust agent"),
ServiceBasedRole(
u"ad_trust_controller_server",
u"AD trust controller",
component_services=['ADTRUST']
),
)
role = roles[0].status(api)[0]
if role.get('status') == 'enabled':
self.trust_agent = True
role = roles[1].status(api)[0]
if role.get('status') == 'enabled':
self.trust_controller = True
registry = IPARegistry()
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/proxy.py 0000664 0000000 0000000 00000010056 14200534377 0023345 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
import logging
import lxml.etree
import re
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipaplatform.paths import paths
logger = logging.getLogger()
def read_ipa_pki_proxy():
"""Read the IPA Proxy configuration file
Split out to make it easier to mock
"""
with open(paths.HTTPD_IPA_PKI_PROXY_CONF, "r") as fd:
lines = fd.readlines()
return lines
@registry
class IPAProxySecretCheck(IPAPlugin):
"""
Ensure that the proxy secrets match between tomcat and Apache
Also report if tomcat has both secret and requiredSecret
defined and whether all three secrets match.
"""
@duration
def check(self):
if not self.ca.is_configured():
logger.debug("CA is not configured, skipping IPAProxySecretCheck")
return
PROXY_SECRETS = 'proxy_secrets'
# so many things can go wrong just keep one big global to
# determine if we can eventually return SUCCESS
failures = False
server_xml = lxml.etree.parse(paths.PKI_TOMCAT_SERVER_XML)
doc = server_xml.getroot()
# no AJP connector means nothing to check
connectors = doc.xpath('//Connector[@protocol="AJP/1.3"]')
if len(connectors) == 0:
yield Result(self, constants.CRITICAL,
key=PROXY_SECRETS,
server_xml=paths.PKI_TOMCAT_SERVER_XML,
msg='No AJP/1.3 Connectors defined in {server_xml}')
return
# IPA only deals with the first connect so that's all we'll check
connector = connectors[0]
ajp_secret = []
if 'secret' in connector.attrib:
ajp_secret.append(connector.attrib['secret'])
if 'requiredSecret' in connector.attrib:
ajp_secret.append(connector.attrib['requiredSecret'])
if len(ajp_secret) > 1:
if ajp_secret[0] != ajp_secret[1]:
failures = True
yield Result(
self, constants.WARNING,
key=PROXY_SECRETS,
server_xml=paths.PKI_TOMCAT_SERVER_XML,
msg='The AJP secrets in {server_xml} do not match'
)
# We could warn that both secret and requiredSecret are defined
# but the presence of both with the same password doesn't
# break anything so we will not warn for now.
lines = read_ipa_pki_proxy()
proxy_secrets = []
PROXY_RE = r'\s+ProxyPassMatch ajp://localhost:8009 secret=(\w+)$'
# Collect all the ipa-pki-proxy.conf secrets and ensure they all match
for line in lines:
m = re.match(PROXY_RE, line)
if m:
proxy_secrets.extend(m.groups(1))
if not proxy_secrets:
failures = True
yield Result(
self, constants.CRITICAL,
key=PROXY_SECRETS,
proxy_conf=paths.HTTPD_IPA_PKI_PROXY_CONF,
msg='No ProxyPassMatch secrets found in {proxy_conf}'
)
return
if len(set(proxy_secrets)) != 1:
failures = True
yield Result(
self, constants.CRITICAL,
key=PROXY_SECRETS,
proxy_conf=paths.HTTPD_IPA_PKI_PROXY_CONF,
msg='Not all ProxyPassMatch secrets match in {proxy_conf}'
)
for secret in proxy_secrets:
if secret not in ajp_secret:
failures = True
yield Result(
self, constants.CRITICAL,
key=PROXY_SECRETS,
server_xml=paths.PKI_TOMCAT_SERVER_XML,
msg='A ProxyPassMatch secret not found in {server_xml}'
)
if not failures:
yield Result(self, constants.SUCCESS,
key=PROXY_SECRETS)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/roles.py 0000664 0000000 0000000 00000003756 14200534377 0023321 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api
logger = logging.getLogger()
@registry
class IPACRLManagerCheck(IPAPlugin):
"""
Determine if this master is the CRL manager
This check in itself will always return SUCCESS and is only
useful in the context of the ohter masters. Some external
service is expected to aggregate this.
"""
@duration
def check(self):
if not self.ca.is_configured():
return
try:
enabled = self.ca.is_crlgen_enabled()
except AttributeError:
yield Result(self, constants.SUCCESS,
key='crl_manager',
crlgen_enabled=None,
msg='Not available in this version of IPA')
else:
yield Result(self, constants.SUCCESS,
key='crl_manager',
crlgen_enabled=enabled)
@registry
class IPARenewalMasterCheck(IPAPlugin):
"""
Determine if this master is the CA renewal master.
This check in itself will always return SUCCESS and is only
useful in the context of the ohter masters. Some external
service is expected to aggregate this.
"""
requires = ('dirsrv',)
@duration
def check(self):
try:
result = api.Command.config_show()
except Exception as e:
yield Result(self, constants.ERROR,
key='renewal_master',
msg='Request for configuration failed, %s' % e)
else:
server = result['result'].get('ca_renewal_master_server', None)
yield Result(self, constants.SUCCESS,
key='renewal_master',
master=server == api.env.host)
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/topology.py 0000664 0000000 0000000 00000005276 14200534377 0024050 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api
logger = logging.getLogger()
@registry
class IPATopologyDomainCheck(IPAPlugin):
"""
Execute the equivalant of ipa topologysuffix-verify domain
Return any errors discovered. This can include:
* too many agreements
* connection errors
"""
def report_errors(self, suffix, result):
if result['result']['in_order']:
yield Result(self, constants.SUCCESS, suffix=suffix)
else:
max_agmts = result['result']['max_agmts']
connect_errors = result['result']['connect_errors']
max_agmts_errors = result['result']['max_agmts_errors']
cmsg = 'Server %(srv)s can\'t contact servers: %(replicas)s'
mmsg = 'Server "%(srv)s" has %(n)d agreements, recommended ' \
'max %(m)d'
if connect_errors:
for error in connect_errors:
msg = cmsg % {'srv': error[0],
'replicas': ', '.join(error[1])}
yield Result(self, constants.ERROR,
key=error[0],
replicas=error[2],
suffix=suffix,
type='connect',
msg=msg)
if max_agmts_errors:
for error in max_agmts_errors:
msg = mmsg % {'srv': error[0],
'n': len(error[1]),
'm': max_agmts}
yield Result(self, constants.ERROR,
key=error[0],
replicas=error[1],
suffix=suffix,
type='max',
msg=msg)
def run_check(self, suffix):
try:
result = api.Command.topologysuffix_verify(suffix)
except Exception as e:
yield Result(self, constants.ERROR,
msg='topologysuffix-verify domain failed, %s' %
e)
else:
for r in self.report_errors(suffix, result):
yield r
requires = ('dirsrv',)
@duration
def check(self):
for y in self.run_check(u'domain'):
yield y
if api.Command.ca_is_enabled()['result']:
for y in self.run_check(u'ca'):
yield y
freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/trust.py 0000664 0000000 0000000 00000057766 14200534377 0023370 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import configparser
import logging
import SSSDConfig
from ipahealthcheck.ipa.plugin import IPAPlugin, registry
from ipahealthcheck.core.plugin import Result
from ipahealthcheck.core.plugin import duration
from ipahealthcheck.core import constants
from ipalib import api
from ipaplatform.paths import paths
from ipapython import ipautil
from ipapython.dn import DN
try:
import pysss_nss_idmap
except ImportError:
# agent and controller will be set to False in init, all tests will
# be skipped
pass
try:
from ipaserver.masters import ENABLED_SERVICE, HIDDEN_SERVICE
except ImportError:
from ipaserver.install.service import ENABLED_SERVICE, HIDDEN_SERVICE
try:
from ipapython.ipaldap import realm_to_serverid
except ImportError:
from ipaserver.install.installutils import realm_to_serverid
logger = logging.getLogger()
def get_trust_domains():
"""
Get the list of AD trust domains from IPA
The caller is expected to catch any exceptions.
Each entry is a dictionary representating an AD domain.
"""
trust_domains = []
trusts = api.Command.trust_find(pkey_only=True, raw=True)
for trust in trusts['result']:
for cn in trust.get('cn'):
trustdomains = api.Command.trustdomain_find(cn, raw=True)
for trustdomain in trustdomains['result']:
domain = dict()
domain['domain'] = trustdomain.get('cn')[0]
domain['domainsid'] = trustdomain.get(
'ipanttrusteddomainsid')[0]
domain['netbios'] = trustdomain.get('ipantflatname')[0]
trust_domains.append(domain)
return trust_domains
@registry
class IPATrustAgentCheck(IPAPlugin):
"""
Check the values that should be set when configures as a trust agent.
"""
@duration
def check(self):
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
try:
sssdconfig = SSSDConfig.SSSDConfig()
sssdconfig.import_config()
except Exception as e:
logger.debug('Failed to parse sssd.conf: %s', e)
yield Result(self, constants.CRITICAL, error=str(e),
msg='Unable to parse sssd.conf: {error}')
return
else:
domains = sssdconfig.list_active_domains()
errors = False
for name in domains:
domain = sssdconfig.get_domain(name)
try:
provider = domain.get_option('id_provider')
except SSSDConfig.NoOptionError:
continue
if provider == "ipa":
try:
mode = domain.get_option('ipa_server_mode')
except SSSDConfig.NoOptionError:
yield Result(self, constants.ERROR,
key='ipa_server_mode_missing',
attr='ipa_server_mode',
domain=name,
sssd_config=paths.SSSD_CONF,
msg='{sssd_config} is missing {attr} '
'in the domain {domain}')
errors = True
else:
if not mode:
yield Result(self, constants.ERROR,
key='ipa_server_mode_false',
attr='ipa_server_mode',
domain=name,
sssd_config=paths.SSSD_CONF,
msg='{attr} is not True in {sssd_config} '
'in the domain {domain}')
errors = True
if not errors:
yield Result(self, constants.SUCCESS)
@registry
class IPATrustDomainsCheck(IPAPlugin):
"""
Check the trust domains
"""
@duration
def check(self):
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
result = ipautil.run([paths.SSSCTL, "domain-list"], raiseonerr=False,
capture_output=True)
if result.returncode != 0:
yield Result(self, constants.ERROR,
key='domain_list_error',
sssctl=paths.SSSCTL,
error=result.error_log,
msg='Execution of {sssctl} failed: {error}')
return
sssd_domains = result.output.strip().split('\n')
if 'implicit_files' in sssd_domains:
sssd_domains.remove('implicit_files')
trust_domains = []
try:
domains = get_trust_domains()
except Exception as e:
yield Result(self, constants.WARNING,
key='trust-find',
error=str(e),
msg='Execution of {key} failed: {error}')
else:
for entry in domains:
trust_domains.append(entry.get('domain'))
if api.env.domain in sssd_domains:
sssd_domains.remove(api.env.domain)
else:
yield Result(self, constants.ERROR,
key=api.env.domain,
sssctl=paths.SSSCTL,
msg='{key} not in {sssctl} domain-list')
trust_domains_out = ', '.join(trust_domains)
sssd_domains_out = ', '.join(sssd_domains)
if set(trust_domains).symmetric_difference(set(sssd_domains)):
yield Result(self, constants.ERROR,
key='domain-list',
sssctl=paths.SSSCTL,
sssd_domains=sssd_domains_out,
trust_domains=trust_domains_out,
msg='{sssctl} {key} reports mismatch: '
'sssd domains {sssd_domains} '
'trust domains {trust_domains}')
else:
yield Result(self, constants.SUCCESS,
key='domain-list',
sssd_domains=sssd_domains_out,
trust_domains=trust_domains_out)
for domain in sssd_domains:
args = [paths.SSSCTL, "domain-status", domain, "--online"]
try:
result = ipautil.run(args, capture_output=True)
except Exception as e:
yield Result(self, constants.WARNING,
key='domain-status',
error=str(e),
msg='Execution of {key} failed: {error}')
continue
else:
if result.output.strip() != 'Online status: Online':
yield Result(self, constants.WARNING,
key='domain-status',
domain=domain,
msg='Domain {domain} is not online')
else:
yield Result(self, constants.SUCCESS,
key='domain-status',
domain=domain)
@registry
class IPADomainCheck(IPAPlugin):
"""
Check that the IPA domain provider is configured to use ipa
"""
@duration
def check(self):
try:
sssdconfig = SSSDConfig.SSSDConfig()
sssdconfig.import_config()
except Exception as e:
logger.debug('Failed to parse sssd.conf: %s', e)
yield Result(self, constants.CRITICAL, error=str(e),
key='domain-check',
msg='Unable to parse sssd.conf: {error}')
return
try:
domain = sssdconfig.get_domain(api.env.domain)
except SSSDConfig.NoDomainError:
yield Result(self, constants.ERROR,
key='domain-check',
domain=api.env.domain,
msg='IPA domain {domain} not found in sssd.conf')
return
error = False
for option in ('id_provider', 'auth_provider', 'chpass_provider',
'access_provider'):
try:
provider = domain.get_option(option)
except SSSDConfig.NoOptionError:
yield Result(self, constants.ERROR,
key='domain-check',
domain=api.env.domain,
option=option,
msg='Option {option} in domain {domain} not '
'found in sssd.conf')
error = True
continue
if provider != "ipa":
yield Result(self, constants.ERROR,
key='domain-check',
option=option,
provider=provider,
domain=api.env.domain,
msg='Option {option} in domain {domain} is '
'{provider} not ipa')
error = True
if not error:
yield Result(self, constants.SUCCESS,
key='domain-check')
@registry
class IPATrustCatalogCheck(IPAPlugin):
"""
Resolve an AD user
This should populate the 'AD Global catalog' and 'AD Domain Controller'
fields in 'sssctl domain-status' output (means SSSD actually talks to AD
DCs)
"""
@duration
def check(self):
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
try:
trust_domains = get_trust_domains()
except Exception as e:
yield Result(self, constants.WARNING,
key='trust-find',
error=str(e),
msg='Execution of {key} failed: {error}')
trust_domains = []
for trust_domain in trust_domains:
sid = trust_domain.get('domainsid')
try:
id = pysss_nss_idmap.getnamebysid(sid + '-500')
except Exception as e:
yield Result(self, constants.ERROR,
key=sid,
error=str(e),
msg='Look up of{key} failed: {error}')
continue
if not id:
yield Result(self, constants.WARNING,
key=sid,
error='returned nothing',
msg='Look up of {key} {error}')
else:
yield Result(self, constants.SUCCESS,
key='Domain Security Identifier',
sid=sid)
domain = trust_domain.get('domain')
args = [paths.SSSCTL, "domain-status", domain, "--active-server"]
try:
result = ipautil.run(args, capture_output=True)
except Exception as e:
yield Result(self, constants.ERROR,
key='domain-status',
error=str(e),
msg='Execution of {key} failed: {error}')
continue
else:
for txt in ['AD Global Catalog', 'AD Domain Controller']:
if txt not in result.output:
yield Result(self, constants.ERROR,
key=txt,
output=result.output.strip(),
sssctl=paths.SSSCTL,
domain=domain,
msg='{key} not found in {sssctl} '
'\'domain-status\' output: {output}')
else:
yield Result(self, constants.SUCCESS,
key=txt,
domain=domain)
@registry
class IPAsidgenpluginCheck(IPAPlugin):
"""
Verify that the sidgen 389-ds plugins are enabled
"""
@duration
def check(self):
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
for plugin in ['IPA SIDGEN', 'ipa-sidgen-task']:
sidgen_dn = DN(('cn', plugin), "cn=plugins,cn=config")
try:
entry = self.conn.get_entry(
sidgen_dn,
attrs_list=['nsslapd-pluginEnabled'])
except Exception as e:
yield Result(self, constants.ERROR,
key=plugin,
error=str(e),
msg='Error retrieving 389-ds plugin {key}: '
'{error}')
else:
enabled = entry.get('nsslapd-pluginEnabled', [])
if len(enabled) != 1:
yield Result(self, constants.ERROR,
key=plugin,
dn=str(sidgen_dn),
attr=enabled,
msg='{key}: unexpected value in '
'nsslapd-pluginEnabled in entry {dn}'
'{attr}')
continue
if entry.get('nsslapd-pluginEnabled', [])[0].lower() != 'on':
yield Result(self, constants.ERROR,
key=plugin,
msg='389-ds plugin {key} is not enabled')
else:
yield Result(self, constants.SUCCESS,
key=plugin)
@registry
class IPATrustAgentMemberCheck(IPAPlugin):
"""
Verify that the current host is a member of adtrust agents
"""
@duration
def check(self):
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
agent_dn = DN(('fqdn', api.env.host), api.env.container_host,
api.env.basedn)
group_dn = DN(('cn', 'adtrust agents'), api.env.container_sysaccounts,
api.env.basedn)
try:
entry = self.conn.get_entry(
agent_dn,
attrs_list=['memberOf'])
except Exception as e:
yield Result(self, constants.ERROR,
key=str(agent_dn),
error=str(e),
msg='Error retrieving ldap entry {key}: '
'{error}')
else:
memberof = entry.get('memberof', [])
for member in memberof:
if DN(member) == group_dn:
yield Result(self, constants.SUCCESS,
key=api.env.host)
return
yield Result(self, constants.ERROR,
key=api.env.host,
group='adtrust agents',
msg='{key} is not a member of {group}')
@registry
class IPATrustControllerPrincipalCheck(IPAPlugin):
"""
Verify that the current host cifs principal is a member of adtrust agents
"""
@duration
def check(self):
if not self.registry.trust_controller:
logger.debug('Not a trust controller, skipping')
return
agent_dn = DN(('krbprincipalname',
'cifs/%s@%s' % (api.env.host, api.env.realm)),
api.env.container_service, api.env.basedn)
group_dn = DN(('cn', 'adtrust agents'), api.env.container_sysaccounts,
api.env.basedn)
try:
entry = self.conn.get_entry(
agent_dn,
attrs_list=['memberOf'])
except Exception as e:
yield Result(self, constants.ERROR,
key=str(agent_dn),
error=str(e),
msg='Error retrieving ldap entry {key}: '
'{error}')
else:
memberof = entry.get('memberof', [])
for member in memberof:
if DN(member) == group_dn:
yield Result(self, constants.SUCCESS,
key='cifs/%s@%s' %
(api.env.host, api.env.realm))
return
yield Result(self, constants.ERROR,
key='cifs/%s@%s' % (api.env.host, api.env.realm),
group='adtrust agents',
msg='{key} is not a member of {group}')
@registry
class IPATrustControllerServiceCheck(IPAPlugin):
"""
Verify that the current host starts the ADTRUST service.
"""
@duration
def check(self):
if not self.registry.trust_controller:
logger.debug('Not a trust controller, skipping')
return
service_dn = DN(('cn', 'ADTRUST'), ('cn', api.env.host),
api.env.container_masters, api.env.basedn)
try:
entry = self.conn.get_entry(
service_dn,
attrs_list=['ipaconfigstring'])
except Exception as e:
yield Result(self, constants.ERROR,
key=str(service_dn),
error=str(e),
msg='Error retrieving ldap entry {key}: '
'{error}')
else:
configs = entry.get('ipaconfigstring', [])
enabled = False
for config in configs:
if config in [ENABLED_SERVICE, HIDDEN_SERVICE]:
enabled = True
break
if enabled:
yield Result(self, constants.SUCCESS,
key='ADTRUST')
else:
yield Result(self, constants.ERROR,
key='ADTRUST',
msg='{key} service is not enabled')
@registry
class IPATrustControllerConfCheck(IPAPlugin):
"""
Verify that certain elements of the configuration are unchanged
This is expected to be expanded over time.
"""
@duration
def check(self):
if not self.registry.trust_controller:
logger.debug('Not a trust controller, skipping')
return
ldapi_socket = "ipasam:ldapi://%%2fvar%%2frun%%2fslapd-%s.socket" % \
realm_to_serverid(api.env.realm)
try:
result = ipautil.run(['net', 'conf', 'list'], capture_output=True)
except Exception as e:
yield Result(self, constants.ERROR,
key='net conf list',
error=str(e),
msg='Execution of {key} failed: {error}')
return
conf = result.output.replace('\t', '')
config = configparser.ConfigParser(delimiters=('='),
interpolation=None)
try:
config.read_string(conf)
except Exception as e:
yield Result(self, constants.ERROR,
key='net conf list',
error=str(e),
msg='Unable to parse {key} output: {error}')
return
try:
net_ldapi = config.get('global', 'passdb backend')
except Exception as e:
yield Result(self, constants.ERROR,
key='net conf list',
error=str(e),
section='global',
option='passdb backend',
msg='Unable to read \'{option}\' in section '
'{section} in {key} output: {error}')
return
if net_ldapi != ldapi_socket:
yield Result(self, constants.ERROR,
key='net conf list',
got=net_ldapi,
expected=ldapi_socket,
option='passdb backend',
msg='{key} option {option} value {got} '
'doesn\'t match expected value {expected}')
else:
yield Result(self, constants.SUCCESS,
key='net conf list')
@registry
class IPATrustControllerGroupSIDCheck(IPAPlugin):
"""
Verify that the admins group's SID ends with 512 (Domain Admins RID)
"""
@duration
def check(self):
if not self.registry.trust_controller:
logger.debug('Not a trust controller, skipping')
return
admins_dn = DN(('cn', 'admins'),
api.env.container_group, api.env.basedn)
try:
entry = self.conn.get_entry(
admins_dn,
attrs_list=['ipantsecurityidentifier'])
except Exception as e:
yield Result(self, constants.ERROR,
key=str(admins_dn),
error=str(e),
msg='Error retrieving ldap entry {key}: '
'{error}')
return
identifier = entry.get('ipantsecurityidentifier', [None])[0]
if not identifier or not identifier.endswith('512'):
yield Result(self, constants.ERROR,
key='ipantsecurityidentifier',
rid=identifier,
msg='{key} is not a Domain Admins RID')
else:
yield Result(self, constants.SUCCESS,
rid=identifier,
key='ipantsecurityidentifier')
@registry
class IPATrustControllerAdminSIDCheck(IPAPlugin):
"""
Verify that the admin user's SID ends with 500
"""
@duration
def check(self):
if not self.registry.trust_controller:
logger.debug('Not a trust controller, skipping')
return
admin_dn = DN(('uid', 'admin'),
api.env.container_user, api.env.basedn)
try:
entry = self.conn.get_entry(
admin_dn,
attrs_list=['ipantsecurityidentifier'])
except Exception as e:
yield Result(self, constants.ERROR,
key=str(admin_dn),
error=str(e),
msg='Error retrieving the admin user at {key}: '
'{error}')
return
identifier = entry.get('ipantsecurityidentifier', [None])[0]
if not identifier or not identifier.endswith('500'):
yield Result(self, constants.ERROR,
key='ipantsecurityidentifier',
rid=identifier,
msg='{key} is not a Domain Admin RID')
else:
yield Result(self, constants.SUCCESS,
rid=identifier,
key='ipantsecurityidentifier')
@registry
class IPATrustPackageCheck(IPAPlugin):
"""
If AD trust is enabled verify that the trust-ad pkg is installed
If AD trust is enabled and the master does not have the
freeipa-server-trust-ad package installed then the master will
able to resolve users/groups via extdom plugin and sssd but won't
be able to do framework-specific operations.
"""
@duration
def check(self):
if self.registry.trust_controller:
logger.debug('Trust controller, skipping')
return
if not self.registry.trust_agent:
logger.debug('Not a trust agent, skipping')
return
# The trust-ad package provides this import
try:
# pylint: disable=unused-import,import-outside-toplevel
from ipaserver.install import adtrustinstance # noqa: F401
# pylint: enable=unused-import,import-outside-toplevel
yield Result(self, constants.SUCCESS,
key='adtrustpackage')
except ImportError:
yield Result(self, constants.WARNING,
key='adtrustpackage',
msg='trust-ad sub-package is not installed. '
'Administration will be limited.')
freeipa-healthcheck-0.10/src/ipahealthcheck/meta/ 0000775 0000000 0000000 00000000000 14200534377 0021765 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/meta/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024064 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/meta/core.py 0000664 0000000 0000000 00000006322 14200534377 0023272 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
import os
import socket
from ipahealthcheck.core import constants
from ipahealthcheck.core.exceptions import TimeoutError
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.meta.plugin import Plugin, registry
from ipapython import ipautil
from ipapython.version import VERSION, API_VERSION
from ipaplatform.paths import paths
if 'FIPS_MODE_SETUP' not in dir(paths):
paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup'
logger = logging.getLogger()
@registry
class MetaCheck(Plugin):
@duration
def check(self):
rval = constants.SUCCESS
if not os.path.exists(paths.FIPS_MODE_SETUP):
fips = "missing {}".format(paths.FIPS_MODE_SETUP)
logger.debug('%s is not installed, skipping',
paths.FIPS_MODE_SETUP)
else:
try:
result = ipautil.run([paths.FIPS_MODE_SETUP,
'--is-enabled'],
capture_output=True,
raiseonerr=False,)
except TimeoutError:
logger.debug('fips-mode-setup timed out')
fips = "check timed out"
rval = constants.ERROR
except Exception as e:
logger.debug('fips-mode-setup failed: %s', e)
fips = "failed to check"
rval = constants.ERROR
else:
logger.debug(result.raw_output.decode('utf-8'))
if result.returncode == 0:
fips = "enabled"
elif result.returncode == 1:
fips = "inconsistent"
elif result.returncode == 2:
fips = "disabled"
else:
fips = "unknown"
if not os.path.exists('/usr/sbin/ipa-acme-manage'):
acme = "missing {}".format('/usr/sbin/ipa-acme-manage')
logger.debug('%s is not installed, skipping',
'/usr/sbin/ipa-acme-manage')
else:
try:
result = ipautil.run(['ipa-acme-manage', 'status'],
capture_output=True,
raiseonerr=False,)
except TimeoutError:
logger.debug('ipa-acme-manage timed out')
acme = "check timed out"
rval = constants.ERROR
except Exception as e:
logger.debug('ipa-acme-manage failed: %s', e)
acme = "failed to check"
rval = constants.ERROR
else:
logger.debug(result.raw_output.decode('utf-8'))
if "disabled" in result.output_log:
acme = "disabled"
elif "enabled" in result.output_log:
acme = "enabled"
else:
acme = "unknown"
yield Result(self, rval,
key='meta',
fqdn=socket.getfqdn(),
fips=fips,
acme=acme,
ipa_version=VERSION,
ipa_api_version=API_VERSION,)
freeipa-healthcheck-0.10/src/ipahealthcheck/meta/plugin.py 0000664 0000000 0000000 00000000351 14200534377 0023634 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core.plugin import Plugin, Registry
class MetaPlugin(Plugin):
pass
class MetaRegistry(Registry):
pass
registry = MetaRegistry()
freeipa-healthcheck-0.10/src/ipahealthcheck/meta/services.py 0000664 0000000 0000000 00000007046 14200534377 0024171 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import logging
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core.service import ServiceCheck
from ipahealthcheck.meta.plugin import registry
try:
from ipapython.ipaldap import realm_to_serverid
except ImportError:
from ipaserver.install.installutils import realm_to_serverid
from ipalib import api
from ipaplatform import services
from ipaserver.install import bindinstance
from ipaserver.install import cainstance
logger = logging.getLogger()
class IPAServiceCheck(ServiceCheck):
@duration
def check(self, instance=''):
try:
# services named with a hyphen cannot be addressed
# as knownservices.name
# so use knownservices['name'] instead
self.service = services.knownservices[self.service_name]
except KeyError:
logger.debug(
"Service '%s' is unknown to ipaplatform, skipping check",
self.service_name
)
return ()
status = self.service.is_running(instance)
if status is False:
yield Result(self, constants.ERROR,
status=status, msg='%s: not running' %
self.service.service_name)
else:
yield Result(self, constants.SUCCESS,
status=status)
return None
@registry
class certmonger(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'certmonger'
return super().check()
@registry
class dirsrv(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'dirsrv'
return super().check(realm_to_serverid(api.env.realm))
@registry
class gssproxy(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'gssproxy'
return super().check()
@registry
class httpd(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'httpd'
return super().check()
@registry
class ipa_custodia(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'ipa-custodia'
return super().check()
@registry
class ipa_dnskeysyncd(IPAServiceCheck):
requires = ('dirsrv',)
def check(self, instance=''):
self.service_name = 'ipa-dnskeysyncd'
if not bindinstance.named_conf_exists():
return ()
return super().check()
@registry
class ipa_otpd(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'ipa-otpd'
return super().check()
@registry
class kadmin(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'kadmin'
return super().check()
@registry
class krb5kdc(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'krb5kdc'
return super().check()
@registry
class named(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'named'
if not bindinstance.named_conf_exists():
return ()
return super().check()
@registry
class pki_tomcatd(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'pki_tomcatd'
ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host)
if not ca.is_configured():
return ()
return super().check()
@registry
class sssd(IPAServiceCheck):
def check(self, instance=''):
self.service_name = 'sssd'
return super().check()
freeipa-healthcheck-0.10/src/ipahealthcheck/system/ 0000775 0000000 0000000 00000000000 14200534377 0022363 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/system/__init__.py 0000664 0000000 0000000 00000000000 14200534377 0024462 0 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/src/ipahealthcheck/system/filesystemspace.py 0000664 0000000 0000000 00000007651 14200534377 0026146 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from __future__ import division
import os
import shutil
from ipahealthcheck.system.plugin import SystemPlugin, registry
from ipahealthcheck.core.plugin import duration, Result
from ipahealthcheck.core import constants
def in_container():
"""Determine if we're running in a container."""
with open('/proc/1/sched', 'r') as sched:
data_sched = sched.readline()
with open('/proc/self/cgroup', 'r') as cgroup:
data_cgroup = cgroup.readline()
checks = [
data_sched.split()[0] not in ('systemd', 'init',),
data_cgroup.split()[0] not in ('libpod'),
os.path.exists('/.dockerenv'),
os.path.exists('/.dockerinit'),
os.getenv('container', None) is not None
]
return any(checks)
@registry
class FileSystemSpaceCheck(SystemPlugin):
"""Check for filesystem available space."""
# watch important directories for FreeIPA
_pathchecks = {
'/var/lib/dirsrv/': 1024,
'/var/lib/ipa/backup/': 512,
'/var/log/': 1024,
'/var/tmp/': 512,
'/tmp': 512
}
if not in_container():
_pathchecks['/var/log/audit/'] = 512
# File systems reaching 90% capacity risk fragmentation.
# Defragmentation is never desirable and not available
# on ext4 anyway. So error out at 20% free space.
min_free_percent = 20
def get_fs_free_space(self, pathname):
stat = shutil.disk_usage(pathname)
return int(stat.free / 2**20)
def get_fs_free_space_percentage(self, pathname):
stat = shutil.disk_usage(pathname)
return int(stat.free * 100 / stat.total)
@duration
def check(self):
for store in self._pathchecks:
try:
percent_free = self.get_fs_free_space_percentage(store)
except FileNotFoundError:
yield Result(
self, constants.WARNING,
key=store,
msg='File system {store} is not mounted',
store=store
)
continue
if percent_free < self.min_free_percent:
yield Result(
self, constants.ERROR,
key=store,
msg='%s: %s %s%% < %s%%' % (
store, 'free space percentage under threshold:',
percent_free, self.min_free_percent
),
store=store, percent_free=percent_free,
threshold=self.min_free_percent
)
else:
yield Result(
self, constants.SUCCESS,
key=store,
msg='%s: %s %s%% >= %s%%' % (
store, 'free space percentage within limits:',
percent_free, self.min_free_percent
),
store=store, percent_free=percent_free,
threshold=self.min_free_percent
)
free_space = self.get_fs_free_space(store)
threshold = self._pathchecks[store]
if free_space < threshold:
yield Result(
self, constants.ERROR,
key=store,
msg='%s: %s %s MiB < %s MiB' % (
store, 'free space under threshold:',
free_space, threshold
),
store=store, free_space=free_space, threshold=threshold
)
else:
yield Result(
self, constants.SUCCESS,
key=store,
msg='%s: %s %s MiB >= %s MiB' % (
store, 'free space within limits:',
free_space, threshold
),
store=store, free_space=free_space, threshold=threshold
)
freeipa-healthcheck-0.10/src/ipahealthcheck/system/plugin.py 0000664 0000000 0000000 00000000456 14200534377 0024240 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core.plugin import Plugin, Registry
class SystemPlugin(Plugin):
pass
class SystemRegistry(Registry):
def initialize(self, framework, config, options=None):
pass
registry = SystemRegistry()
freeipa-healthcheck-0.10/systemd/ 0000775 0000000 0000000 00000000000 14200534377 0017003 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/systemd/ipa-healthcheck.service 0000664 0000000 0000000 00000000226 14200534377 0023377 0 ustar 00root root 0000000 0000000 [Unit]
Description=Execute IPA Healthcheck
[Service]
Type=simple
ExecStart=/usr/libexec/ipa/ipa-healthcheck.sh
[Install]
WantedBy=multi-user.target
freeipa-healthcheck-0.10/systemd/ipa-healthcheck.sh 0000775 0000000 0000000 00000000153 14200534377 0022353 0 ustar 00root root 0000000 0000000 #!/bin/sh
LOGDIR=/var/log/ipa/healthcheck
/usr/bin/ipa-healthcheck --output-file $LOGDIR/healthcheck.log
freeipa-healthcheck-0.10/systemd/ipa-healthcheck.timer 0000664 0000000 0000000 00000000242 14200534377 0023055 0 ustar 00root root 0000000 0000000 [Unit]
Description=Execute IPA Healthcheck every day at 4AM
[Timer]
OnCalendar=*-*-* 04:00:00
Unit=ipa-healthcheck.service
[Install]
WantedBy=multi-user.target
freeipa-healthcheck-0.10/tests/ 0000775 0000000 0000000 00000000000 14200534377 0016455 5 ustar 00root root 0000000 0000000 freeipa-healthcheck-0.10/tests/base.py 0000664 0000000 0000000 00000003377 14200534377 0017753 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from unittest import mock, TestCase
from util import no_exceptions
from util import ADtrustBasedRole, ServiceBasedRole
class BaseTest(TestCase):
"""
Base class for tests.
Most tests use the same set of mocks so centralize and apply them
once when the class of tests is created.
A child class defines self.patches as a dictionary of functions
and Mock values. These are applied once when the class starts up.
If a test needs a particular value then it will need to use
@patch individually.
A default set of Mock patches is set because they apply to all or
nearly all test cases.
"""
default_patches = {
'ipaserver.install.installutils.check_server_configuration':
mock.Mock(return_value=None),
'ipaserver.servroles.ServiceBasedRole':
mock.Mock(return_value=ServiceBasedRole()),
'ipaserver.servroles.ADtrustBasedRole':
mock.Mock(return_value=ADtrustBasedRole()),
}
patches = {}
results = None
applied_patches = None
def setup_class(self):
# collect the list of patches to be applied for this class of
# tests
self.default_patches.update(self.patches)
self.applied_patches = [
mock.patch(patch, data) for patch, data in
self.default_patches.items()
]
for patch in self.applied_patches:
patch.start()
def teardown_class(self):
mock.patch.stopall()
def tearDown(self):
"""
Ensure that no exceptions snuck into the results which might not
be noticed because an exception may have the same result as
the expected result.
"""
no_exceptions(self.results)
freeipa-healthcheck-0.10/tests/clusterdata.py 0000664 0000000 0000000 00000053716 14200534377 0021356 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
ONE_MASTER = {
'ipa.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "ipa.ipa.example",
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
]
}
},
# No RUV's on a freshly installed master
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
}
},
]
}
THREE_MASTERS_OK = {
'ipa.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "ipa.ipa.example",
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "4"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "6"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
],
[
"replica1.ipa.example",
"3"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
],
[
"replica1.ipa.example",
"5"
]
]
}
}
],
'replica1.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica1.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "3"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "5"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica1.ipa.example",
"3"
],
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica1.ipa.example",
"5"
],
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
]
]
}
}
],
'replica2.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica2.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "7"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "8"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica2.ipa.example",
"7"
],
[
"ipa.ipa.example",
"4"
],
[
"replica1.ipa.example",
"3"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica2.ipa.example",
"8"
],
[
"ipa.ipa.example",
"6"
],
[
"replica1.ipa.example",
"5"
]
]
}
}
]
}
#
# Same three masters but replica1 has an extra RUV value
#
THREE_MASTERS_BAD_IPA_RUV = {
'ipa.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "ipa.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "4"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "6"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
],
[
"replica1.ipa.example",
"3"
],
[
"replica1.ipa.example",
"9"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
],
[
"replica1.ipa.example",
"5"
]
]
}
}
],
'replica1.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica1.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "3"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "5"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica1.ipa.example",
"3"
],
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica1.ipa.example",
"5"
],
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
]
]
}
}
],
'replica2.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica2.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "7"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "8"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica2.ipa.example",
"7"
],
[
"ipa.ipa.example",
"4"
],
[
"replica1.ipa.example",
"3"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica2.ipa.example",
"8"
],
[
"ipa.ipa.example",
"6"
],
[
"replica1.ipa.example",
"5"
]
]
}
}
]
}
#
# Same three masters but replica2 CA has an extra RUV value
#
THREE_MASTERS_BAD_CS_RUV = {
'ipa.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "ipa.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "4"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "6"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
],
[
"replica1.ipa.example",
"3"
],
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
],
[
"replica1.ipa.example",
"5"
]
]
}
}
],
'replica1.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica1.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "3"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "5"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica1.ipa.example",
"3"
],
[
"ipa.ipa.example",
"4"
],
[
"replica2.ipa.example",
"7"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica1.ipa.example",
"5"
],
[
"ipa.ipa.example",
"6"
],
[
"replica2.ipa.example",
"8"
]
]
}
}
],
'replica2.ipa.example': [
{
"source": "ipahealthcheck.meta.core",
"check": "MetaCheck",
"result": "SUCCESS",
"kw": {
"fqdn": "replica2.ipa.example",
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example",
],
"ipa_version": "4.8.4",
"ipa_api_version": "2.235"
}
},
{
"source": "ipahealthcheck.ipa.meta",
"check": "IPAMetaCheck",
"result": "SUCCESS",
"kw": {
"masters": [
"ipa.ipa.example",
"replica1.ipa.example",
"replica2.ipa.example"
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "dc=ipa,dc=example",
"ruv": "7"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "RUVCheck",
"result": "SUCCESS",
"kw": {
"key": "o=ipaca",
"ruv": "8"
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_dc=ipa,dc=example",
"suffix": "dc=ipa,dc=example",
"ruvs": [
[
"replica2.ipa.example",
"7"
],
[
"ipa.ipa.example",
"4"
],
[
"replica1.ipa.example",
"3"
]
]
}
},
{
"source": "ipahealthcheck.ds.ruv",
"check": "KnownRUVCheck",
"result": "SUCCESS",
"kw": {
"key": "ruvs_o=ipaca",
"suffix": "o=ipaca",
"ruvs": [
[
"replica2.ipa.example",
"8"
],
[
"ipa.ipa.example",
"6"
],
[
"replica1.ipa.example",
"5"
],
[
"replica1.ipa.example",
"9"
]
]
}
}
]
}
freeipa-healthcheck-0.10/tests/mock_certmonger.py 0000664 0000000 0000000 00000011213 14200534377 0022203 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import copy
from datetime import datetime, timedelta, timezone
from ipaplatform.paths import paths
# Fake certmonger tracked request list. This is similar but can be
# distinct from the value from the overrident get_defaults() method.
template = paths.CERTMONGER_COMMAND_TEMPLATE
CERT_EXPIRATION_DAYS = 30
pristine_cm_requests = [
{
'nickname': '1234',
'cert-file': paths.RA_AGENT_PEM,
'key-file': paths.RA_AGENT_KEY,
'ca-name': 'dogtag-ipa-ca-renew-agent',
'template_profile': 'caSubsystemCert',
'cert-storage': 'FILE',
'cert-presave-command': template % 'renew_ra_cert_pre',
'cert-postsave-command': template % 'renew_ra_cert',
'not-valid-after': (
int(
datetime(1970, 1, 1, 0, 17, 4, tzinfo=timezone.utc).timestamp()
)
),
},
{
'nickname': '5678',
'cert-file': paths.HTTPD_CERT_FILE,
'key-file': paths.HTTPD_KEY_FILE,
'ca-name': 'IPA',
'template_profile': 'caIPAserviceCert',
'cert-storage': 'FILE',
'cert-postsave-command': template % 'restart_httpd',
'not-valid-after': (
int(
(
datetime.now(timezone.utc) +
timedelta(days=CERT_EXPIRATION_DAYS + 1)
).timestamp()
)
),
},
]
class dbus_results:
"""Class to manage the results returned by dbus"""
def __init__(self):
self.requests = copy.deepcopy(pristine_cm_requests)
def __iter__(self):
for entry in self.requests:
yield entry
def __len__(self):
return len(self.requests)
def __getitem__(self, index):
return self.requests[index]
def append(self, entry):
self.requests.append(entry)
def remove(self, index):
self.requests.remove(self.requests[index])
def __repr__(self):
return repr(self.requests)
cm_requests = []
class mock_property:
def __init__(self, index):
self.index = index
def Get(self, object_path, name):
"""Always return a match"""
if self.index is None:
return None
return cm_requests[self.index].get(name)
class mock_dbus:
"""Create a fake dbus representation of a tracked certificate
The index is used to look up values within the cm_requests
list of known tracked certificates.
"""
def __init__(self, request_id):
self.index = None
for i, cm_request in enumerate(cm_requests):
if request_id == cm_request.get('nickname'):
self.index = i
break
self.prop_if = mock_property(self.index)
self.obj_if = mock_obj_if(self.index)
class mock_obj_if:
def __init__(self, index):
self.index = index
def find_request_by_nickname(self, nickname):
return None
def get_requests(self):
"""Return list of request ids that dbus would have returned"""
return [n.get('nickname') for n in cm_requests]
def get_nickname(self):
"""Retrieve the certmonger CA nickname"""
if self.index is None:
return None
return cm_requests[self.index].get('ca-name')
def get_ca(self):
"""Return the CA name for the current request"""
return cm_requests[self.index].get('nickname')
class _certmonger:
"""An empty object, not needed directly for testing
Needed to keep the real certmonger from blowing up.
"""
def __init__(self):
self.obj_if = mock_obj_if(None)
self.bus = None
def create_mock_dbus(bus, parent, object_path, object_dbus_interface,
parent_dbus_interface=None, property_interface=False):
"""Create a fake dbus object for a given path (request_id)"""
return mock_dbus(object_path)
def get_expected_requests():
"""The list of requests known by the IPACertCheck plugin
The list is copied and the nickname popped off to match the
format that the check uses.
nickname has two meanings in certmonger: the request id and
the NSS nickname.
"""
requests = copy.deepcopy(pristine_cm_requests)
for request in requests:
try:
request.pop('nickname')
request.pop('not-valid-after')
except KeyError:
pass
return requests
def set_requests(add=None, remove=None):
"""Set the list of requests within a test"""
global cm_requests
cm_requests = dbus_results()
if add is not None:
cm_requests.append(add)
if remove is not None:
cm_requests.remove(remove)
freeipa-healthcheck-0.10/tests/test_cluster_ruv.py 0000664 0000000 0000000 00000006603 14200534377 0022450 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from util import capture_results
from ipahealthcheck.core import config
from ipaclustercheck.ipa.plugin import ClusterRegistry
from ipaclustercheck.ipa.ruv import ClusterRUVCheck
import clusterdata
class RUVRegistry(ClusterRegistry):
def load_files(self, dir):
self.json = dir
class Options:
def __init__(self, data):
self.data = data
@property
def dir(self):
return self.data
registry = RUVRegistry()
class TestClusterRUV(BaseTest):
def test_no_ruvs(self):
"""Single master test that has never created a replica
This type of master will have no RUVs created at all.
"""
framework = object()
registry.initialize(framework, config.Config,
Options(clusterdata.ONE_MASTER))
f = ClusterRUVCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.kw.get('name') == 'dangling_ruv'
assert result.kw.get('value') == 'No dangling RUVs found'
result = self.results.results[1]
assert result.kw.get('name') == 'dangling_csruv'
assert result.kw.get('value') == 'No dangling CS RUVs found'
def test_six_ruvs_ok(self):
"""Three master test with each having a CA, no dangling
"""
framework = object()
registry.initialize(framework, config.Config,
Options(clusterdata.THREE_MASTERS_OK))
f = ClusterRUVCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.kw.get('name') == 'dangling_ruv'
assert result.kw.get('value') == 'No dangling RUVs found'
result = self.results.results[1]
assert result.kw.get('name') == 'dangling_csruv'
assert result.kw.get('value') == 'No dangling CS RUVs found'
def test_six_ruvs_ipa_bad(self):
"""Three master test with each having a CA, dangling IPA RUV
"""
framework = object()
registry.initialize(framework, config.Config,
Options(clusterdata.THREE_MASTERS_BAD_IPA_RUV))
f = ClusterRUVCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.kw.get('name') == 'dangling_ruv'
assert result.kw.get('value') == '9'
result = self.results.results[1]
assert result.kw.get('name') == 'dangling_csruv'
assert result.kw.get('value') == 'No dangling CS RUVs found'
def test_six_ruvs_cs_bad(self):
"""Three master test with each having a CA, dangling CA RUV
"""
framework = object()
registry.initialize(framework, config.Config,
Options(clusterdata.THREE_MASTERS_BAD_CS_RUV))
f = ClusterRUVCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.kw.get('name') == 'dangling_ruv'
assert result.kw.get('value') == 'No dangling RUVs found'
result = self.results.results[1]
assert result.kw.get('name') == 'dangling_csruv'
assert result.kw.get('value') == '9'
freeipa-healthcheck-0.10/tests/test_commands.py 0000664 0000000 0000000 00000003251 14200534377 0021670 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
import os
from ipapython.ipautil import run
import pytest
def test_version():
"""
Test the --version option
"""
output = run(['ipa-healthcheck', '--version'], env=os.environ)
assert 'ipahealthcheck' in output.raw_output.decode('utf-8')
@pytest.fixture
def python_ipalib_dir(tmpdir):
ipalib_dir = tmpdir.mkdir("ipalib")
ipalib_dir.join("__init__.py").write("")
def _make_facts(configured=None):
if configured is None:
module_text = ""
elif isinstance(configured, bool):
module_text = f"def is_ipa_configured(): return {configured}"
else:
raise TypeError(
f"'configured' must be None or bool, got '{configured!r}'"
)
ipalib_dir.join("facts.py").write(module_text)
return str(tmpdir)
return _make_facts
def test_ipa_notinstalled(python_ipalib_dir, monkeypatch):
"""
Test ipa-healthcheck handles the missing IPA stuff
"""
monkeypatch.setenv("PYTHONPATH", python_ipalib_dir(configured=None))
output = run(["ipa-healthcheck"], raiseonerr=False, env=os.environ)
assert output.returncode == 1
assert "IPA server is not installed" in output.raw_output.decode("utf-8")
def test_ipa_unconfigured(python_ipalib_dir, monkeypatch):
"""
Test ipa-healthcheck handles the unconfigured IPA server
"""
monkeypatch.setenv("PYTHONPATH", python_ipalib_dir(configured=False))
output = run(["ipa-healthcheck"], raiseonerr=False, env=os.environ)
assert output.returncode == 1
assert "IPA server is not configured" in output.raw_output.decode("utf-8")
freeipa-healthcheck-0.10/tests/test_config.py 0000664 0000000 0000000 00000002323 14200534377 0021333 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import tempfile
import pytest
from ipahealthcheck.core.config import read_config
def test_config_no_section():
with tempfile.NamedTemporaryFile('w') as f:
f.write('\n')
f.flush()
config = read_config(f.name)
assert config is None
def test_config_bad_format():
with tempfile.NamedTemporaryFile('w') as f:
f.write('bad\n')
config = read_config(f.name)
f.flush()
assert config is None
def test_config_values():
with tempfile.NamedTemporaryFile('w') as f:
f.write('[default]\nfoo = bar\n')
f.flush()
config = read_config(f.name)
assert config.foo == 'bar'
with pytest.raises(KeyError):
config.bar # pylint: disable=pointless-statement
def test_config_recursion():
with tempfile.NamedTemporaryFile('w') as f:
f.write('[default]\nfoo = bar\n')
f.flush()
config = read_config(f.name)
assert config.foo == 'bar'
# The config dict is in the object
assert isinstance(config._Config__d, dict)
# But it isn't recursive
try:
config._Config__d['_Config__d']
except KeyError:
pass
freeipa-healthcheck-0.10/tests/test_core_files.py 0000664 0000000 0000000 00000012370 14200534377 0022203 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import pwd
import posix
from ipahealthcheck.core.files import FileCheck
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Results
from unittest.mock import patch
from util import capture_results
nobody = pwd.getpwnam('nobody')
# Mock files to test
files = (('foo', 'root', 'root', '0660'),
('bar', 'nobody', 'nobody', '0664'),
('baz', ('root', 'nobody'), ('root', 'nobody'), '0664'),
('fiz', ('root', 'bin'), ('root', 'bin'), '0664'),)
def make_stat(mode=33200, uid=0, gid=0):
"""Return a mocked-up stat.
The default is:
mode = 0660
owner = root
group = root
"""
# (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime)
return posix.stat_result((mode, 1, 42, 1, uid, gid, 0, 1, 1, 1,))
def get_results(results, type):
"""Pull out the type of results I want to look at: owner, group or mode"""
my_results = Results()
for r in results.results:
kw = r.kw
if kw.get('type') != type:
continue
my_results.add(r)
return my_results
@patch('os.stat')
def test_files_owner(mock_stat):
"""
Test the file owner.
Our mocked files want root, nobody, (root, nobody), (root, root).
"""
f = FileCheck()
f.files = files
mock_stat.return_value = make_stat()
results = capture_results(f)
my_results = get_results(results, 'owner')
assert my_results.results[0].result == constants.SUCCESS
assert my_results.results[1].result == constants.WARNING
assert my_results.results[2].result == constants.SUCCESS
assert my_results.results[3].result == constants.SUCCESS
mock_stat.return_value = make_stat(uid=nobody.pw_uid)
results = capture_results(f)
my_results = get_results(results, 'owner')
assert my_results.results[0].result == constants.WARNING
assert my_results.results[0].kw.get('got') == 'nobody'
assert my_results.results[0].kw.get('expected') == 'root'
assert my_results.results[0].kw.get('type') == 'owner'
assert my_results.results[1].result == constants.SUCCESS
assert my_results.results[2].result == constants.SUCCESS
assert my_results.results[3].result == constants.WARNING
assert my_results.results[3].kw.get('got') == 'nobody'
assert my_results.results[3].kw.get('expected') == 'root,bin'
assert my_results.results[3].kw.get('type') == 'owner'
assert my_results.results[3].kw.get('msg') == \
'Ownership of fiz is nobody and should be one of root,bin'
@patch('os.stat')
def test_files_group(mock_stat):
"""
Test the file group.
Our mocked files want root, nobody, (root, nobody), (root, root).
"""
f = FileCheck()
f.files = files
mock_stat.return_value = make_stat()
results = capture_results(f)
my_results = get_results(results, 'group')
assert my_results.results[0].result == constants.SUCCESS
assert my_results.results[1].result == constants.WARNING
assert my_results.results[2].result == constants.SUCCESS
assert my_results.results[3].result == constants.SUCCESS
mock_stat.return_value = make_stat(gid=nobody.pw_gid)
results = capture_results(f)
my_results = get_results(results, 'group')
assert my_results.results[0].result == constants.WARNING
assert my_results.results[0].kw.get('got') == 'nobody'
assert my_results.results[0].kw.get('expected') == 'root'
assert my_results.results[0].kw.get('type') == 'group'
assert my_results.results[1].result == constants.SUCCESS
assert my_results.results[2].result == constants.SUCCESS
assert my_results.results[3].result == constants.WARNING
assert my_results.results[3].kw.get('got') == 'nobody'
assert my_results.results[3].kw.get('expected') == 'root,bin'
assert my_results.results[3].kw.get('type') == 'group'
assert my_results.results[3].kw.get('msg') == \
'Group of fiz is nobody and should be one of root,bin'
@patch('os.stat')
def test_files_mode(mock_stat):
mock_stat.return_value = make_stat()
f = FileCheck()
f.files = files
results = capture_results(f)
my_results = get_results(results, 'mode')
assert my_results.results[0].result == constants.SUCCESS
assert my_results.results[1].result == constants.ERROR
mock_stat.return_value = make_stat(mode=33152)
results = capture_results(f)
my_results = get_results(results, 'mode')
assert my_results.results[0].result == constants.ERROR
assert my_results.results[1].result == constants.ERROR
mock_stat.return_value = make_stat(mode=33206)
results = capture_results(f)
my_results = get_results(results, 'mode')
assert my_results.results[0].result == constants.WARNING
assert my_results.results[1].result == constants.WARNING
@patch('os.path.exists')
def test_files_not_found(mock_exists):
mock_exists.return_value = False
f = FileCheck()
f.files = files
results = capture_results(f)
for type in ('mode', 'group', 'owner'):
my_results = get_results(results, type)
assert len(my_results.results) == 4
for result in my_results.results:
assert result.result == constants.SUCCESS
assert result.kw.get('msg') == 'File does not exist'
freeipa-healthcheck-0.10/tests/test_dogtag_ca.py 0000664 0000000 0000000 00000011112 14200534377 0021772 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance, KRAInstance
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.dogtag.plugin import registry
from ipahealthcheck.dogtag.ca import DogtagCertsConfigCheck
from unittest.mock import Mock, patch
class mock_Cert:
"""Fake up a certificate.
The contents are the NSS nickname of the certificate.
"""
def __init__(self, text):
self.text = text
def public_bytes(self, encoding):
return self.text.encode('utf-8')
class mock_CertDB:
def __init__(self, trust):
"""A dict of nickname + NSSdb trust flags"""
self.trust = trust
def list_certs(self):
return [(nickname, self.trust[nickname]) for nickname in self.trust]
def get_cert_from_db(self, nickname):
"""Return the nickname. This will match the value of get_directive"""
return mock_Cert(nickname)
class TestCACerts(BaseTest):
patches = {
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
'ipaserver.install.krainstance.KRAInstance':
Mock(return_value=KRAInstance()),
}
@patch('ipahealthcheck.dogtag.ca.get_directive')
@patch('ipaserver.install.certs.CertDB')
def test_ca_certs_ok(self, mock_certdb, mock_directive):
"""Test what should be the standard case"""
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u',
'caSigningCert cert-pki-ca': 'CT,C,C',
'transportCert cert-pki-kra': 'u,u,u',
}
mock_certdb.return_value = mock_CertDB(trust)
mock_directive.side_effect = [name for name, nsstrust in trust.items()]
framework = object()
registry.initialize(framework, config.Config())
f = DogtagCertsConfigCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 6
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConfigCheck'
@patch('ipahealthcheck.dogtag.ca.get_directive')
@patch('ipaserver.install.certs.CertDB')
def test_cert_missing_from_file(self, mock_certdb, mock_directive):
"""Test a missing certificate.
Note that if it is missing from the database then this check
will not catch the error but it will be caught elsewhere.
"""
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u',
'caSigningCert cert-pki-ca': 'CT,,',
'transportCert cert-pki-kra': 'u,u,u',
}
# The 3rd cert won't match the results
nicknames = [name for name, nsstrust in trust.items()]
location = nicknames.index('auditSigningCert cert-pki-ca')
nicknames[location] = 'NOT auditSigningCert cert-pki-ca'
mock_certdb.return_value = mock_CertDB(trust)
mock_directive.side_effect = nicknames
framework = object()
registry.initialize(framework, config.Config)
f = DogtagCertsConfigCheck(registry)
self.results = capture_results(f)
num = len(self.results.results)
for r in range(0, num):
if r == 2: # skip the one that should be bad
continue
result = self.results.results[r]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConfigCheck'
result = self.results.results[2]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConfigCheck'
assert result.kw.get('key') == 'auditSigningCert cert-pki-ca'
assert len(self.results) == 6
@patch('ipaserver.install.cainstance.CAInstance')
def test_cacert_caless(self, mock_cainstance):
"""Nothing to check if the master is CALess"""
mock_cainstance.return_value = CAInstance(False)
framework = object()
registry.initialize(framework, config)
f = DogtagCertsConfigCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 0
freeipa-healthcheck-0.10/tests/test_dogtag_connectivity.py 0000664 0000000 0000000 00000005462 14200534377 0024140 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance
from util import m_api
from base import BaseTest
from ipahealthcheck.core import constants, config
from ipahealthcheck.dogtag.plugin import registry
from ipahealthcheck.dogtag.ca import DogtagCertsConnectivityCheck
from unittest.mock import Mock
from ipalib.errors import CertificateOperationError
class TestCAConnectivity(BaseTest):
patches = {
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
}
def test_ca_connection_ok(self):
"""CA connectivity check when cert_show returns a valid value"""
m_api.Command.cert_show.side_effect = None
m_api.Command.cert_show.return_value = {
u'result': {u'revoked': False}
}
framework = object()
registry.initialize(framework, config.Config)
f = DogtagCertsConnectivityCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConnectivityCheck'
def test_ca_connection_cert_not_found(self):
"""CA connectivity check for a cert that doesn't exist"""
m_api.Command.cert_show.reset_mock()
m_api.Command.cert_show.side_effect = CertificateOperationError(
message='Certificate operation cannot be completed: '
'EXCEPTION (Certificate serial number 0x0 not found)'
)
framework = object()
registry.initialize(framework, config.Config)
f = DogtagCertsConnectivityCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConnectivityCheck'
def test_ca_connection_down(self):
"""CA connectivity check with the CA down"""
m_api.Command.cert_show.side_effect = CertificateOperationError(
message='Certificate operation cannot be completed: '
'Unable to communicate with CMS (503)'
)
framework = object()
registry.initialize(framework, config.Config)
f = DogtagCertsConnectivityCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConnectivityCheck'
assert 'Unable to communicate' in result.kw.get('msg')
freeipa-healthcheck-0.10/tests/test_ds_ruv.py 0000664 0000000 0000000 00000010153 14200534377 0021370 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import Mock
from util import capture_results, m_api
from ipahealthcheck.core import config, constants
from ipahealthcheck.ds.plugin import registry
from ipahealthcheck.ds.ruv import RUVCheck
from ipalib import errors
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, LDAPEntry
class mock_ldap:
SCOPE_BASE = 1
SCOPE_ONELEVEL = 2
SCOPE_SUBTREE = 4
def __init__(self, ldapentry):
"""Initialize the results that we will return from get_entries"""
self.results = ldapentry
self.index = 0
def get_entry(self, dn, attrs_list=None, time_limit=None,
size_limit=None, get_effective_rights=False):
if len(self.results) == 0:
raise errors.NotFound(reason='test')
self.index += 1
if self.results[self.index - 1] is None:
raise errors.NotFound(reason='test')
return self.results[self.index - 1]
class mock_ldap_conn:
def set_option(self, option, invalue):
pass
def search_s(self, base, scope, filterstr=None,
attrlist=None, attrsonly=0):
return tuple()
class TestRUV(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def create_entry(self, conn, dn, attrs):
"""Create an LDAPEntry object from the provided dn and attrs
dn: DN() object
attrs: dict of name/value pairs of LDAP attributes
"""
ldapentry = LDAPEntry(conn, dn)
for attr, values in attrs.items():
ldapentry[attr] = values
return ldapentry
def test_no_ruvs(self):
framework = object()
registry.initialize(framework, config.Config)
f = RUVCheck(registry)
f.conn = mock_ldap(None)
self.results = capture_results(f)
assert len(self.results) == 0
def test_both_ruvs(self):
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
entries = []
entries.append(
self.create_entry(fake_conn,
DN('dc=example,cn=mapping tree,cn=config'),
{'nsds5ReplicaId': ['3']})
)
entries.append(
self.create_entry(fake_conn,
DN('o=ipaca,cn=mapping tree,cn=config'),
{'nsds5ReplicaId': ['5']})
)
framework = object()
registry.initialize(framework, config.Config)
f = RUVCheck(registry)
f.conn = mock_ldap(entries)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ds.ruv'
assert result.check == 'RUVCheck'
assert result.kw.get('key') == str(m_api.env.basedn)
assert result.kw.get('ruv') == '3'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ds.ruv'
assert result.check == 'RUVCheck'
assert result.kw.get('key') == 'o=ipaca'
assert result.kw.get('ruv') == '5'
def test_one_ruvs(self):
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
entries = []
entries.append(
self.create_entry(fake_conn,
DN('dc=example,cn=mapping tree,cn=config'),
{'nsds5ReplicaId': ['3']})
)
entries.append(None)
framework = object()
registry.initialize(framework, config.Config)
f = RUVCheck(registry)
f.conn = mock_ldap(entries)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ds.ruv'
assert result.check == 'RUVCheck'
assert result.kw.get('key') == str(m_api.env.basedn)
assert result.kw.get('ruv') == '3'
freeipa-healthcheck-0.10/tests/test_init.py 0000664 0000000 0000000 00000001426 14200534377 0021034 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
import argparse
from ipahealthcheck.core.output import output_registry
class RunChecks:
def run_healthcheck(self):
options = argparse.Namespace(check=None, debug=False, indent=2,
list_sources=False, output_file=None,
output_type='json', source=None,
verbose=False)
for out in output_registry.plugins:
if out.__name__.lower() == options.output_type:
out(options)
break
def test_run_healthcheck():
"""
Test typical initialization in run_healthcheck (based ok pki-healthcheck)
"""
run = RunChecks()
run.run_healthcheck()
freeipa-healthcheck-0.10/tests/test_ipa_agent.py 0000664 0000000 0000000 00000030253 14200534377 0022020 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import Mock, patch
from util import capture_results, CAInstance, KRAInstance
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPARAAgent, IPAKRAAgent
from ipalib import errors
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, LDAPEntry
from ipaplatform.paths import paths
from ldap import OPT_X_SASL_SSF_MIN
class IPACertificate:
def __init__(self, serial_number=1):
self.subject = 'CN=RA AGENT'
self.issuer = 'CN=ISSUER'
self.serial_number = serial_number
class mock_ldap:
SCOPE_BASE = 1
SCOPE_ONELEVEL = 2
SCOPE_SUBTREE = 4
def __init__(self, ldapentry):
"""Initialize the results that we will return from get_entries"""
self.results = ldapentry
def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None,
attrs_list=None, get_effective_rights=False, **kwargs):
if self.results is None:
raise errors.NotFound(reason='test')
return self.results
class mock_ldap_conn:
def set_option(self, option, invalue):
pass
def get_option(self, option):
if option == OPT_X_SASL_SSF_MIN:
return 256
return None
def search_s(self, base, scope, filterstr=None,
attrlist=None, attrsonly=0):
return tuple()
class TestNSSAgent(BaseTest):
cert = IPACertificate()
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
'ipalib.x509.load_certificate_from_file':
Mock(return_value=cert),
}
def test_nss_agent_ok(self):
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPARAAgent'
def test_nss_agent_no_description(self):
attrs = dict(
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert 'description' in result.kw.get('msg')
@patch('ipalib.x509.load_certificate_from_file')
def test_nss_agent_load_failure(self, mock_load_cert):
mock_load_cert.side_effect = IOError('test')
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('error') == 'test'
def test_nss_agent_no_entry_found(self):
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
f.conn = mock_ldap(None) # None == NotFound
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('msg') == 'RA agent not found in LDAP'
def test_nss_agent_too_many(self):
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
ldapentry2 = LDAPEntry(fake_conn, DN('uid=ipara2,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
f.conn = mock_ldap([ldapentry, ldapentry2])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('found') == 2
def test_nss_agent_nonmatching_cert(self):
cert2 = IPACertificate(2)
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[cert2],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPARAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('certfile') == paths.RA_AGENT_PEM
assert result.kw.get('dn') == 'uid=ipara,ou=people,o=ipaca'
def test_nss_agent_multiple_certs(self):
cert2 = IPACertificate(2)
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[cert2, self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
f = IPARAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPARAAgent'
class TestKRAAgent(BaseTest):
cert = IPACertificate()
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
'ipaserver.install.krainstance.KRAInstance':
Mock(return_value=KRAInstance()),
'ipalib.x509.load_certificate_from_file':
Mock(return_value=cert),
}
def test_kra_agent_ok(self):
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPAKRAAgent'
def test_kra_agent_no_description(self):
attrs = dict(
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert 'description' in result.kw.get('msg')
@patch('ipalib.x509.load_certificate_from_file')
def test_kra_agent_load_failure(self, mock_load_cert):
mock_load_cert.side_effect = IOError('test')
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('error') == 'test'
def test_kra_agent_no_entry_found(self):
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
f.conn = mock_ldap(None) # None == NotFound
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('msg') == 'KRA agent not found in LDAP'
def test_kra_agent_too_many(self):
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
ldapentry2 = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
f.conn = mock_ldap([ldapentry, ldapentry2])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('found') == 2
def test_kra_agent_nonmatching_cert(self):
cert2 = IPACertificate(2)
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[cert2],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config())
f = IPAKRAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('certfile') == paths.RA_AGENT_PEM
assert result.kw.get('dn') == 'uid=ipakra,ou=people,o=kra,o=ipaca'
def test_kra_agent_multiple_certs(self):
cert2 = IPACertificate(2)
attrs = dict(
description=['2;1;CN=ISSUER;CN=RA AGENT'],
usercertificate=[cert2, self.cert],
)
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn,
DN('uid=ipakra,ou=people,o=kra,o=ipaca'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
f = IPAKRAAgent(registry)
f.conn = mock_ldap([ldapentry])
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPAKRAAgent'
freeipa-healthcheck-0.10/tests/test_ipa_cert_match.py 0000664 0000000 0000000 00000025324 14200534377 0023036 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
from util import capture_results, m_api, CAInstance, KRAInstance
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertMatchCheck
from ipahealthcheck.ipa.certs import IPADogtagCertsMatchCheck
from unittest.mock import Mock, patch
from ipalib import errors
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, LDAPEntry
class IPACertificate:
def __init__(self, serial_number=1):
self.serial_number = serial_number
def __eq__(self, other):
return self.serial_number == other.serial_number
def __hash__(self):
return hash(self.serial_number)
class mock_ldap:
SCOPE_BASE = 1
SCOPE_ONELEVEL = 2
SCOPE_SUBTREE = 4
def __init__(self, entries):
"""Initialize the results that we will return from get_entry"""
self.results = {entry.dn: entry for entry in entries}
def get_entry(self, dn, attrs_list=None, time_limit=None,
size_limit=None, get_effective_rights=False):
if self.results is None:
raise errors.NotFound(reason='test')
return self.results[dn]
def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None,
attrs_list=None, get_effective_rights=False, **kwargs):
if self.results is None:
raise errors.NotFound(reason='test')
return self.results.values()
class mock_ldap_conn:
def set_option(self, option, invalue):
pass
def search_s(self, base, scope, filterstr=None,
attrlist=None, attrsonly=0):
return tuple()
class mock_CertDB:
def __init__(self, trust):
"""A dict of nickname + NSSdb trust flags"""
self.trust = trust
self.secdir = '/foo/bar/testdir'
def get_cert_from_db(self, nickname):
if nickname not in self.trust.keys():
raise errors.NotFound(reason='test')
return IPACertificate()
def run_certutil(self, args, capture_output):
class RunResult:
def __init__(self, output):
self.raw_output = output
return RunResult(b'test output')
class TestIPACertMatch(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn())
}
@patch('ipalib.x509.load_certificate_list_from_file')
@patch('ipaserver.install.certs.CertDB')
def test_certs_match_ok(self, mock_certdb, mock_load_cert):
""" Ensure match check is ok"""
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
cacertentry = LDAPEntry(fake_conn,
DN('cn=%s IPA CA' % m_api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
m_api.env.basedn),
CACertificate=[IPACertificate()])
trust = {
('%s IPA CA' % m_api.env.realm): 'u,u,u'
}
mock_certdb.return_value = mock_CertDB(trust)
mock_load_cert.return_value = [IPACertificate()]
framework = object()
registry.initialize(framework, config.Config())
f = IPACertMatchCheck(registry)
f.conn = mock_ldap([cacertentry])
self.results = capture_results(f)
assert len(self.results) == 3
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertMatchCheck'
@patch('ipalib.x509.load_certificate_list_from_file')
@patch('ipaserver.install.certs.CertDB')
def test_etc_cacert_mismatch(self, mock_certdb, mock_load_cert):
""" Test mismatch with /etc/ipa/ca.crt """
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
cacertentry = LDAPEntry(fake_conn,
DN('cn=%s IPA CA' % m_api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
m_api.env.basedn),
CACertificate=[IPACertificate()])
trust = {
('%s IPA CA' % m_api.env.realm): 'u,u,u'
}
mock_certdb.return_value = mock_CertDB(trust)
mock_load_cert.return_value = [IPACertificate(serial_number=2)]
framework = object()
registry.initialize(framework, config.Config())
f = IPACertMatchCheck(registry)
f.conn = mock_ldap([cacertentry])
self.results = capture_results(f)
assert len(self.results) == 3
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertMatchCheck'
@patch('ipaserver.install.cainstance.CAInstance')
def test_cacert_caless(self, mock_cainstance):
"""Nothing to check if the master is CALess"""
mock_cainstance.return_value = CAInstance(False)
framework = object()
registry.initialize(framework, config)
f = IPACertMatchCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 0
class TestIPADogtagCertMatch(BaseTest):
patches = {
'ipaserver.install.krainstance.KRAInstance':
Mock(return_value=KRAInstance()),
}
@patch('ipaserver.install.certs.CertDB')
def test_certs_match_ok(self, mock_certdb):
""" Ensure match check is ok"""
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
pkidbentry = LDAPEntry(fake_conn,
DN('uid=pkidbuser,ou=people,o=ipaca'),
userCertificate=[IPACertificate()],
subjectName=['test'])
casignentry = LDAPEntry(fake_conn,
DN('cn=%s IPA CA' % m_api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
m_api.env.basedn),
CACertificate=[IPACertificate()],
userCertificate=[IPACertificate()],
subjectName=['test'])
ldap_entries = [pkidbentry, casignentry]
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'caSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u',
'transportCert cert-pki-kra': 'u,u,u',
'storageCert cert-pki-kra': 'u,u,u',
'auditSigningCert cert-pki-kra': 'u,u,Pu',
}
dogtag_entries_subjects = (
'CN=OCSP Subsystem,O=%s' % m_api.env.realm,
'CN=CA Subsystem,O=%s' % m_api.env.realm,
'CN=CA Audit,O=%s' % m_api.env.realm,
'CN=%s,O=%s' % (m_api.env.host, m_api.env.realm),
'CN=KRA Transport Certificate,O=%s' % m_api.env.realm,
'CN=KRA Storage Certificate,O=%s' % m_api.env.realm,
'CN=KRA Audit,O=%s' % m_api.env.realm,
)
for i, subject in enumerate(dogtag_entries_subjects):
entry = LDAPEntry(fake_conn,
DN('cn=%i,ou=certificateRepository' % i,
'ou=ca,o=ipaca'),
userCertificate=[IPACertificate()],
subjectName=[subject])
ldap_entries.append(entry)
mock_certdb.return_value = mock_CertDB(trust)
framework = object()
registry.initialize(framework, config.Config())
f = IPADogtagCertsMatchCheck(registry)
f.conn = mock_ldap(ldap_entries)
self.results = capture_results(f)
assert len(self.results) == 3
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPADogtagCertsMatchCheck'
@patch('ipaserver.install.certs.CertDB')
def test_certs_mismatch(self, mock_certdb):
""" Ensure mismatches are detected"""
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
pkidbentry = LDAPEntry(fake_conn,
DN('uid=pkidbuser,ou=people,o=ipaca'),
userCertificate=[IPACertificate(
serial_number=2
)],
subjectName=['test'])
casignentry = LDAPEntry(fake_conn,
DN('cn=%s IPA CA' % m_api.env.realm,
'cn=certificates,cn=ipa,cn=etc',
m_api.env.basedn),
CACertificate=[IPACertificate()],
userCertificate=[IPACertificate()],
subjectName=['test'])
ldap_entries = [pkidbentry, casignentry]
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'caSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u',
'transportCert cert-pki-kra': 'u,u,u',
'storageCert cert-pki-kra': 'u,u,u',
'auditSigningCert cert-pki-kra': 'u,u,Pu',
}
dogtag_entries_subjects = (
'CN=OCSP Subsystem,O=%s' % m_api.env.realm,
'CN=CA Subsystem,O=%s' % m_api.env.realm,
'CN=CA Audit,O=%s' % m_api.env.realm,
'CN=%s,O=%s' % (m_api.env.host, m_api.env.realm),
'CN=KRA Transport Certificate,O=%s' % m_api.env.realm,
'CN=KRA Storage Certificate,O=%s' % m_api.env.realm,
'CN=KRA Audit,O=%s' % m_api.env.realm,
)
for i, subject in enumerate(dogtag_entries_subjects):
entry = LDAPEntry(fake_conn,
DN('cn=%i,ou=certificateRepository' % i,
'ou=ca,o=ipaca'),
userCertificate=[IPACertificate()],
subjectName=[subject])
ldap_entries.append(entry)
mock_certdb.return_value = mock_CertDB(trust)
framework = object()
registry.initialize(framework, config.Config())
f = IPADogtagCertsMatchCheck(registry)
f.conn = mock_ldap(ldap_entries)
self.results = capture_results(f)
assert len(self.results) == 3
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPADogtagCertsMatchCheck'
freeipa-healthcheck-0.10/tests/test_ipa_certfile_expiration.py 0000664 0000000 0000000 00000007252 14200534377 0024764 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertfileExpirationCheck
from unittest.mock import Mock, patch
from mock_certmonger import create_mock_dbus, _certmonger
from mock_certmonger import (
get_expected_requests,
set_requests,
CERT_EXPIRATION_DAYS,
)
from datetime import datetime, timedelta
class IPACertificate:
def __init__(self, not_valid_after, serial_number=1):
self.subject = 'CN=RA AGENT'
self.issuer = 'CN=ISSUER'
self.serial_number = serial_number
self.not_valid_after = not_valid_after
class TestIPACertificateFile(BaseTest):
patches = {
'ipahealthcheck.ipa.certs.get_expected_requests':
Mock(return_value=get_expected_requests()),
'ipalib.install.certmonger._cm_dbus_object':
Mock(side_effect=create_mock_dbus),
'ipalib.install.certmonger._certmonger':
Mock(return_value=_certmonger()),
}
@patch('ipalib.x509.load_certificate_from_file')
def test_certfile_expiration(self, mock_load_cert):
set_requests(remove=1)
cert = IPACertificate(not_valid_after=datetime.utcnow() +
timedelta(days=CERT_EXPIRATION_DAYS))
mock_load_cert.return_value = cert
framework = object()
registry.initialize(framework, config.Config)
f = IPACertfileExpirationCheck(registry)
f.config.cert_expiration_days = '28'
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertfileExpirationCheck'
assert result.kw.get('key') == '1234'
@patch('ipalib.x509.load_certificate_from_file')
def test_certfile_expiration_warning(self, mock_load_cert):
set_requests(remove=1)
cert = IPACertificate(not_valid_after=datetime.utcnow() +
timedelta(days=7))
mock_load_cert.return_value = cert
framework = object()
registry.initialize(framework, config.Config)
f = IPACertfileExpirationCheck(registry)
f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertfileExpirationCheck'
assert result.kw.get('key') == '1234'
assert result.kw.get('days') == 6
@patch('ipalib.x509.load_certificate_from_file')
def test_certfile_expiration_expired(self, mock_load_cert):
set_requests(remove=1)
cert = IPACertificate(not_valid_after=datetime.utcnow() +
timedelta(days=-100))
mock_load_cert.return_value = cert
framework = object()
registry.initialize(framework, config.Config)
f = IPACertfileExpirationCheck(registry)
f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertfileExpirationCheck'
assert result.kw.get('key') == '1234'
assert 'expiration_date' in result.kw
freeipa-healthcheck-0.10/tests/test_ipa_certmonger_ca.py 0000664 0000000 0000000 00000003725 14200534377 0023536 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance
from base import BaseTest
from ipahealthcheck.core import constants, config
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertmongerCA
from unittest.mock import Mock, patch
class TestCertmonger(BaseTest):
patches = {
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
}
@patch('ipahealthcheck.ipa.certs.IPACertmongerCA.find_ca')
def test_certmogner_ok(self, mock_find_ca):
mock_find_ca.side_effect = [
'IPA',
'dogtag-ipa-ca-renew-agent',
'dogtag-ipa-ca-renew-agent-reuse'
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACertmongerCA(registry)
self.results = capture_results(f)
assert len(self.results) == 3
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerCA'
@patch('ipahealthcheck.ipa.certs.IPACertmongerCA.find_ca')
def test_certmogner_missing(self, mock_find_ca):
mock_find_ca.side_effect = [
'IPA',
'dogtag-ipa-ca-renew-agent',
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACertmongerCA(registry)
self.results = capture_results(f)
assert len(self.results) == 3
for r in range(0, 1):
result = self.results.results[r]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerCA'
assert self.results.results[2].result == constants.ERROR
assert self.results.results[2].kw.get('key') == \
'dogtag-ipa-ca-renew-agent-reuse'
freeipa-healthcheck-0.10/tests/test_ipa_dna.py 0000664 0000000 0000000 00000006336 14200534377 0021471 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import patch
from util import capture_results
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.dna import IPADNARangeCheck
class mock_ReplicationManager:
def __init__(self, realm=None, host=None, start=None, max=None,
next=None, next_max=None):
self.start = start
self.max = max
self.next = next
self.next_max = next_max
def get_DNA_range(self, host):
return self.start, self.max
def get_DNA_next_range(self, host):
return self.next, self.next_max
class TestDNARange(BaseTest):
@patch('ipaserver.install.replication.ReplicationManager')
def test_dnarange_set(self, mock_manager):
mock_manager.return_value = mock_ReplicationManager(start=1, max=100)
framework = object()
registry.initialize(framework, config.Config)
f = IPADNARangeCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.dna'
assert result.check == 'IPADNARangeCheck'
assert result.kw.get('range_start') == 1
assert result.kw.get('range_max') == 100
assert result.kw.get('next_start') == 0
assert result.kw.get('next_max') == 0
@patch('ipaserver.install.replication.ReplicationManager')
def test_dnarange_noset(self, mock_manager):
mock_manager.return_value = mock_ReplicationManager()
framework = object()
registry.initialize(framework, config.Config)
f = IPADNARangeCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.dna'
assert result.check == 'IPADNARangeCheck'
assert result.kw.get('range_start') == 0
assert result.kw.get('range_max') == 0
assert result.kw.get('next_start') == 0
assert result.kw.get('next_max') == 0
@patch('ipaserver.install.replication.ReplicationManager')
def test_dnarange_next(self, mock_manager):
mock_manager.return_value = mock_ReplicationManager(start=1,
max=100,
next=101,
next_max=200)
framework = object()
registry.initialize(framework, config.Config)
f = IPADNARangeCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.dna'
assert result.check == 'IPADNARangeCheck'
assert result.kw.get('range_start') == 1
assert result.kw.get('range_max') == 100
assert result.kw.get('next_start') == 101
assert result.kw.get('next_max') == 200
freeipa-healthcheck-0.10/tests/test_ipa_dns.py 0000664 0000000 0000000 00000067762 14200534377 0021525 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import re
from dns import (
rdata,
rdataclass,
rdatatype,
message,
rrset,
version,
)
from dns.resolver import Answer
from base import BaseTest
from util import capture_results, m_api
from unittest.mock import patch
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.idns import IPADNSSystemRecordsCheck
from ipapython.dnsutil import DNSName
from ipaserver.dns_data_management import (
IPA_DEFAULT_MASTER_SRV_REC,
IPA_DEFAULT_ADTRUST_SRV_REC
)
try:
# pylint: disable=unused-import
from ipaserver.dns_data_management import IPA_DEFAULT_MASTER_URI_REC # noqa
except ImportError:
has_uri_support = False
else:
has_uri_support = True
try:
# pylint: disable=unused-import
from ipaserver.install.installutils import resolve_rrsets_nss # noqa: F401
# pylint: enable=unused-import
except ImportError:
resolve_rrsets_import = 'ipaserver.dns_data_management.resolve_rrsets'
else:
resolve_rrsets_import = 'ipaserver.install.installutils.resolve_rrsets_nss'
def add_srv_records(qname, port_map, priority=0, weight=100):
rdlist = []
for _unused, port in port_map:
answerlist = []
for host in qname:
hostname = DNSName(host)
rd = rdata.from_text(
rdataclass.IN, rdatatype.SRV,
'{0} {1} {2} {3}'.format(
priority, weight, port, hostname.make_absolute()
)
)
answerlist.append(rd)
rdlist.append(answerlist)
return rdlist
def resolve_rrsets(fqdn, rdtypes):
"""
Return an A record for the hostname in an RRset type in a list.
"""
rset = []
for rdtype in rdtypes:
rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN,
rdtype, gen_addrs(rdtype, 1))
rset.append(rlist)
return rset
def query_srv(qname, ad_records=False):
"""
Return a SRV for each service IPA cares about for all the hosts.
This is pre-generated as a side-effect for each test.
"""
rdlist = add_srv_records(qname, IPA_DEFAULT_MASTER_SRV_REC)
if ad_records:
rdlist.extend(add_srv_records(qname, IPA_DEFAULT_ADTRUST_SRV_REC))
return rdlist
def query_uri(hosts):
"""
Return a list containing two answers, one for each uri type
"""
answers = []
if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0):
m = message.Message()
elif version.MAJOR == 2 and version.MINOR > 0:
m = message.QueryMessage() # pylint: disable=E1101
m = message.make_response(m) # pylint: disable=E1101
rdtype = rdatatype.URI
for name in ('_kerberos.', '_kpasswd.'):
qname = DNSName(name + m_api.env.domain)
qname = qname.make_absolute()
if version.MAJOR < 2:
# pylint: disable=unexpected-keyword-arg
answer = Answer(qname, rdataclass.IN, rdtype, m,
raise_on_no_answer=False)
# pylint: enable=unexpected-keyword-arg
else:
if version.MAJOR == 2 and version.MINOR > 0:
question = rrset.RRset(qname, rdataclass.IN, rdtype)
m.question = [question]
answer = Answer(qname, rdataclass.IN, rdtype, m)
rl = []
for host in hosts:
rlist = rrset.from_text_list(
qname, 86400, rdataclass.IN,
rdatatype.URI, ['0 100 "krb5srv:m:tcp:%s."' % host,
'0 100 "krb5srv:m:udp:%s."' % host, ]
)
rl.extend(rlist)
answer.rrset = rl
answers.append(answer)
return answers
def gen_addrs(rdtype=rdatatype.A, num=1):
"""Generate sequential IP addresses for the ipa-ca A record lookup"""
ips = []
if rdtype == rdatatype.A:
ip_template = '192.168.0.%d'
if rdtype == rdatatype.AAAA:
ip_template = '2001:db8:1::%d'
for i in range(num):
ips.append(ip_template % (i + 1))
return ips
def fake_query(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1,
fake_txt=False):
"""Fake a DNS query, returning count responses to the request
Three kinds of lookups are faked:
1. A query for A/AAAA records for a service will return the count
as requested in the test. This simulates lookups for the
ipa-ca A/AAAA record. To force a difference in responses one can
vary the count.
2. TXT queries will return the Kerberos realm
fake_txt will set an invalid Kerberos realm entry to provoke a
warning.
"""
if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0):
m = message.Message()
elif version.MAJOR == 2 and version.MINOR > 0:
m = message.QueryMessage() # pylint: disable=E1101
m = message.make_response(m) # pylint: disable=E1101
if rdtype in (rdatatype.A, rdatatype.AAAA):
fqdn = DNSName(qname)
fqdn = fqdn.make_absolute()
if version.MAJOR < 2:
# pylint: disable=unexpected-keyword-arg
answers = Answer(fqdn, rdataclass.IN, rdtype, m,
raise_on_no_answer=False)
# pylint: enable=unexpected-keyword-arg
else:
if version.MAJOR == 2 and version.MINOR > 0:
question = rrset.RRset(fqdn, rdataclass.IN, rdtype)
m.question = [question]
answers = Answer(fqdn, rdataclass.IN, rdtype, m)
rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN,
rdtype, gen_addrs(rdtype, count))
answers.rrset = rlist
elif rdtype == rdatatype.TXT:
if fake_txt:
realm = 'FAKE_REALM'
else:
realm = m_api.env.realm
qname = DNSName('_kerberos.' + m_api.env.domain)
qname = qname.make_absolute()
if version.MAJOR < 2:
# pylint: disable=unexpected-keyword-arg
answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m,
raise_on_no_answer=False)
# pylint: enable=unexpected-keyword-arg
else:
if version.MAJOR == 2 and version.MINOR > 0:
question = rrset.RRset(qname, rdataclass.IN, rdtype)
m.question = [question]
answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m)
rlist = rrset.from_text_list(qname, 86400, rdataclass.IN,
rdatatype.TXT, [realm])
answers.rrset = rlist
return answers
# Helpers to generate an appropriate number of A records for the
# ipa-ca and Kerberos realm responses. Optionally return a bogus
# TXT record.
def fake_query_one(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN,
count=1):
return fake_query(qname, rdtype, rdclass, count)
def fake_query_two(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN,
count=2):
return fake_query(qname, rdtype, rdclass, count)
def fake_query_three(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN,
count=3):
return fake_query(qname, rdtype, rdclass, count)
def fake_query_one_txt(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN,
count=1):
return fake_query(qname, rdtype, rdclass, count, fake_txt=True)
def get_results_by_severity(results, severity):
"""Return the results with a matching severity"""
new_results = []
for result in results:
if result.result == severity:
new_results.append(result)
return new_results
class TestDNSSystemRecords(BaseTest):
"""Test that the SRV records checks are working properly
The intention was to not override IPASystemRecords since
this is the core mechanism that IPA uses to determine what
recoreds should exist.
Instead the DNS lookups are managed. This is done in two
ways:
1. The query_srv() override returns the set of configured
servers for each type of SRV record.
2. fake_query() overrides ipahealthcheck.ipa.idns.resolve to simulate
A, AAAA and TXT record lookups.
"""
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_single(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Test single CA master, all SRV records"""
mock_query.side_effect = fake_query_one
mock_query_srv.side_effect = query_srv([m_api.env.host])
mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 14
else:
expected = 10
assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.idns'
assert result.check == 'IPADNSSystemRecordsCheck'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_two(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Test two CA masters, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain
])
mock_query.side_effect = fake_query_two
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 27
else:
expected = 19
assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.idns'
assert result.check == 'IPADNSSystemRecordsCheck'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_three(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Test three CA masters, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica2.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica2.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 40
else:
expected = 28
assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.idns'
assert result.check == 'IPADNSSystemRecordsCheck'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_three_mixed(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Test three masters, only one with a CA, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query.side_effect = fake_query_one
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica2.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA))
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'IPA master'
],
},
{
'cn': ['replica2.' + m_api.env.domain],
'enabled_role_servrole': [
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 36
else:
expected = 24
assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.idns'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_missing_server(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Drop one of the masters from query_srv
This will simulate missing SRV records and cause a number of
warnings to be thrown.
"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain
# replica2 is missing
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica2.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica2.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 40
else:
expected = 28
assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
if has_uri_support:
assert len(ok) == 33
assert len(warn) == 7
else:
assert len(ok) == 21
assert len(warn) == 7
for result in warn:
assert result.kw.get('msg') == 'Expected SRV record missing'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Drop one of the masters from query_srv
This will simulate missing SRV records and cause a number of
warnings to be thrown.
"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
mock_query.side_effect = fake_query_two
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica2.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA))
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica2.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 40
else:
expected = 28
assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
if has_uri_support:
assert len(ok) == 38
assert len(warn) == 2
else:
assert len(ok) == 26
assert len(warn) == 2
for result in warn:
assert re.match(
r'^Got {count} ipa-ca (A|AAAA) records, expected {expected}$',
result.kw.get('msg')
)
assert result.kw.get('count') == 2
assert result.kw.get('expected') == 3
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_extra_srv(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""An extra SRV record set exists, report it.
Add an extra master to the query_srv() which will generate
a full extra set of SRV records for the master.
"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain,
'replica3.' + m_api.env.domain
])
mock_query_uri.side_effect = query_uri([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain,
])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica2.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
resolve_rrsets('replica3.' + m_api.env.domain,
(rdatatype.A, rdatatype.AAAA)),
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
{
'cn': ['replica2.' + m_api.env.domain],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 47
else:
expected = 35
assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
if has_uri_support:
assert len(ok) == 40
assert len(warn) == 7
else:
assert len(ok) == 28
assert len(warn) == 7
for result in warn:
assert result.kw.get('msg') == \
'Unexpected SRV entry in DNS'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_bad_realm(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
"""Unexpected Kerberos TXT record"""
mock_query.side_effect = fake_query_one_txt
mock_query_srv.side_effect = query_srv([m_api.env.host])
mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 14
else:
expected = 10
assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
if has_uri_support:
assert len(ok) == 13
assert len(warn) == 1
else:
assert len(ok) == 9
assert len(warn) == 1
result = warn[0]
assert result.kw.get('msg') == 'expected realm missing'
assert result.kw.get('key') == '\"FAKE_REALM\"'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
@patch('ipahealthcheck.ipa.idns.query_uri')
@patch('ipahealthcheck.ipa.idns.resolve')
def test_dnsrecords_one_with_ad(self, mock_query, mock_query_uri,
mock_query_srv, mock_rrset):
mock_query.side_effect = fake_query_one
mock_query_srv.side_effect = query_srv([m_api.env.host], True)
mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
m_api.Command.server_find.side_effect = [{
'result': [
{
'cn': [m_api.env.host],
'enabled_role_servrole': [
'CA server',
'IPA master',
'AD trust controller'
],
},
]
}]
framework = object()
registry.initialize(framework, config.Config)
f = IPADNSSystemRecordsCheck(registry)
self.results = capture_results(f)
if has_uri_support:
expected = 20
else:
expected = 16
assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.idns'
assert result.check == 'IPADNSSystemRecordsCheck'
freeipa-healthcheck-0.10/tests/test_ipa_dnssan.py 0000664 0000000 0000000 00000007700 14200534377 0022211 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance
from util import m_api
from base import BaseTest
from unittest.mock import Mock, patch
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertDNSSAN
from mock_certmonger import create_mock_dbus, _certmonger
from mock_certmonger import get_expected_requests, set_requests
class IPACertificate:
def __init__(self, serial_number=1, no_san=False):
self.subject = 'CN=%s' % m_api.env.host
self.issuer = 'CN=ISSUER'
self.serial_number = serial_number
self.san_a_label_dns_names = [m_api.env.host]
if not no_san:
self.san_a_label_dns_names.append('ipa-ca.%s' % m_api.env.domain)
class TestDNSSAN(BaseTest):
patches = {
'ipaserver.install.certs.is_ipa_issued_cert':
Mock(return_value=True),
'ipahealthcheck.ipa.certs.get_expected_requests':
Mock(return_value=get_expected_requests()),
'ipalib.install.certmonger._cm_dbus_object':
Mock(side_effect=create_mock_dbus),
'ipalib.install.certmonger._certmonger':
Mock(return_value=_certmonger()),
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
'socket.getfqdn':
Mock(return_value=m_api.env.host),
}
@patch('ipalib.install.certmonger.get_request_value')
@patch('ipalib.x509.load_certificate_from_file')
def test_dnssan_ok(self, mock_cert, mock_value):
set_requests()
mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
'IPA', 'caIPAserviceCert']
mock_cert.return_value = IPACertificate()
framework = object()
registry.initialize(framework, config.Config)
f = IPACertDNSSAN(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.kw.get('san') == [m_api.env.host,
'ipa-ca.%s' % m_api.env.domain]
assert result.kw.get('hostname') == [m_api.env.host,
'ipa-ca.%s' % m_api.env.domain]
assert result.kw.get('profile') == 'caIPAserviceCert'
assert result.check == 'IPACertDNSSAN'
@patch('ipalib.install.certmonger.get_request_value')
def test_sandns_no_certs(self, mock_value):
set_requests()
mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
'dogtag-ipa-ca-renew-agent']
framework = object()
registry.initialize(framework, config.Config)
f = IPACertDNSSAN(registry)
self.results = capture_results(f)
# No IPA CA, no results
assert len(self.results) == 0
@patch('ipalib.install.certmonger.get_request_value')
@patch('ipalib.x509.load_certificate_from_file')
def test_dnssan_missing_ipaca(self, mock_cert, mock_value):
set_requests()
mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
'IPA', 'caIPAserviceCert']
mock_cert.return_value = IPACertificate(no_san=True)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertDNSSAN(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.kw.get('san') == [m_api.env.host]
assert result.kw.get('hostname') == 'ipa-ca.%s' % m_api.env.domain
assert result.kw.get('profile') == 'caIPAserviceCert'
assert result.kw.get('ca') == 'IPA'
assert result.kw.get('key') == '5678'
freeipa-healthcheck-0.10/tests/test_ipa_expiration.py 0000664 0000000 0000000 00000021734 14200534377 0023110 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results
from base import BaseTest
from ipaplatform.paths import paths
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertmongerExpirationCheck
from ipahealthcheck.ipa.certs import IPACAChainExpirationCheck
from unittest.mock import Mock, patch
from mock_certmonger import create_mock_dbus, _certmonger
from mock_certmonger import (
get_expected_requests,
set_requests,
CERT_EXPIRATION_DAYS,
)
from datetime import datetime, timedelta, timezone
class TestExpiration(BaseTest):
patches = {
'ipahealthcheck.ipa.certs.get_expected_requests':
Mock(return_value=get_expected_requests()),
'ipalib.install.certmonger._cm_dbus_object':
Mock(side_effect=create_mock_dbus),
'ipalib.install.certmonger._certmonger':
Mock(return_value=_certmonger())
}
def test_expiration(self):
set_requests()
framework = object()
registry.initialize(framework, config.Config)
f = IPACertmongerExpirationCheck(registry)
f.config.cert_expiration_days = '7'
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerExpirationCheck'
assert result.kw.get('key') == '1234'
assert result.kw.get('expiration_date') == '19700101001704Z'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerExpirationCheck'
assert result.kw.get('key') == '5678'
def test_expiration_warning(self):
warning = datetime.now(timezone.utc) + timedelta(days=20)
replaceme = {
'nickname': '7777',
'cert-file': paths.RA_AGENT_PEM,
'key-file': paths.RA_AGENT_KEY,
'ca-name': 'dogtag-ipa-ca-renew-agent',
'not-valid-after': int(warning.timestamp()),
}
set_requests(remove=0, add=replaceme)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertmongerExpirationCheck(registry)
f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerExpirationCheck'
assert result.kw.get('key') == '5678'
result = self.results.results[1]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertmongerExpirationCheck'
assert result.kw.get('key') == '7777'
assert result.kw.get('days') == 19
class FakeIPACertificate:
def __init__(self, cert, backend=None, subject=None, not_after=None):
self.subj = subject
self.not_after = not_after
@property
def subject(self):
return self.subj
@property
def not_valid_after(self):
return self.not_after
class TestChainExpiration(BaseTest):
root_ca = 'CN=Certificate Shack Root CA,O=Certificate Shack Ltd'
sub_ca = 'CN=Certificate Shack Intermediate CA,O=Certificate Shack Ltd'
@patch('ipalib.x509.load_certificate_list_from_file')
def test_still_valid(self, mock_load):
mock_load.return_value = [
FakeIPACertificate(
None,
subject=self.sub_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=20)
),
FakeIPACertificate(
None,
subject=self.root_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=20)
)
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACAChainExpirationCheck(registry)
f.config.cert_expiration_days = '7'
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.sub_ca
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.root_ca
@patch('ipalib.x509.load_certificate_list_from_file')
def test_expiring_soon(self, mock_load):
mock_load.return_value = [
FakeIPACertificate(
None,
subject=self.sub_ca,
not_after=datetime.now(timezone.utc) +
timedelta(days=3, minutes=1)
),
FakeIPACertificate(
None,
subject=self.root_ca,
not_after=datetime.now(timezone.utc) +
timedelta(days=3, minutes=1)
)
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACAChainExpirationCheck(registry)
f.config.cert_expiration_days = '7'
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.sub_ca
assert result.kw.get('days') == 3
assert 'expiring' in result.kw.get('msg')
result = self.results.results[1]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.root_ca
assert result.kw.get('days') == 3
assert 'expiring' in result.kw.get('msg')
@patch('ipalib.x509.load_certificate_list_from_file')
def test_all_expired(self, mock_load):
mock_load.return_value = [
FakeIPACertificate(
None,
subject=self.sub_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=-3)
),
FakeIPACertificate(
None,
subject=self.root_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=-3)
)
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACAChainExpirationCheck(registry)
f.config.cert_expiration_days = '7'
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.sub_ca
assert 'expired' in result.kw.get('msg')
result = self.results.results[1]
assert result.result == constants.CRITICAL
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.root_ca
assert 'expired' in result.kw.get('msg')
@patch('ipalib.x509.load_certificate_list_from_file')
def test_one_expired(self, mock_load):
mock_load.return_value = [
FakeIPACertificate(
None,
subject=self.sub_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=-3)
),
FakeIPACertificate(
None,
subject=self.root_ca,
not_after=datetime.now(timezone.utc) + timedelta(days=20)
)
]
framework = object()
registry.initialize(framework, config.Config)
f = IPACAChainExpirationCheck(registry)
f.config.cert_expiration_days = '7'
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.sub_ca
assert 'expired' in result.kw.get('msg')
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACAChainExpirationCheck'
assert result.kw.get('key') == self.root_ca
freeipa-healthcheck-0.10/tests/test_ipa_nss.py 0000664 0000000 0000000 00000004325 14200534377 0021526 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from collections import namedtuple
from unittest.mock import patch
from util import capture_results
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.nss import IPAGroupMemberCheck
struct_group = namedtuple(
'struct_group', ['gr_name', 'gr_passwd', 'gr_gid', 'gr_mem']
)
def make_group(name, members):
return struct_group(name, 'x', 999, members)
class TestGroupMember(BaseTest):
@patch('grp.getgrnam')
def test_ipaapi_group_ok(self, mock_grp):
mock_grp.return_value = make_group('apache', ('apache', 'ipaapi',))
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPAGroupMemberCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
@patch('grp.getgrnam')
def test_ipaapi_bad_group(self, mock_grp):
mock_grp.side_effect = KeyError("name not found: 'ipaapi'")
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPAGroupMemberCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'ipaapi'
assert result.kw.get('msg') == 'group {key} does not exist'
@patch('grp.getgrnam')
def test_ipaapi_missing_member(self, mock_grp):
mock_grp.return_value = make_group('apache', ('foo',))
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPAGroupMemberCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'ipaapi'
assert result.kw.get('msg') == \
'{member} is not a member of group {key}'
freeipa-healthcheck-0.10/tests/test_ipa_nssdb.py 0000664 0000000 0000000 00000012215 14200534377 0022031 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance, KRAInstance
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertNSSTrust
from ipaplatform.paths import paths
from unittest.mock import Mock, patch
class mock_CertDB:
def __init__(self, trust):
"""A dict of nickname + NSSdb trust flags"""
self.trust = trust
def list_certs(self):
return [(nickname, self.trust[nickname]) for nickname in self.trust]
def my_unparse_trust_flags(trust_flags):
return trust_flags
# These tests make some assumptions about the order in which the
# results are returned.
class TestNSSDBTrust(BaseTest):
patches = {
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
'ipaserver.install.krainstance.KRAInstance':
Mock(return_value=KRAInstance(False)),
'ipapython.certdb.unparse_trust_flags':
Mock(side_effect=my_unparse_trust_flags),
}
@patch('ipaserver.install.certs.CertDB')
def test_trust_default_ok(self, mock_certdb):
"""Test what should be the standard case"""
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u'
}
mock_certdb.return_value = mock_CertDB(trust)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertNSSTrust(registry)
self.results = capture_results(f)
assert len(self.results) == 4
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertNSSTrust'
assert 'cert-pki-ca' in result.kw.get('key')
@patch('ipaserver.install.certs.CertDB')
def test_trust_ocsp_missing(self, mock_certdb):
"""Test a missing certificate"""
trust = {
'subsystemCert cert-pki-ca': 'u,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'u,u,u'
}
mock_certdb.return_value = mock_CertDB(trust)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertNSSTrust(registry)
self.results = capture_results(f)
# The check reports success for those that it found and are correct and
# reports missing certs last.
num = len(self.results.results) - 2
for r in range(0, num):
result = self.results.results[r]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertNSSTrust'
assert 'cert-pki-ca' in result.kw.get('key')
result = self.results.results[-1]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertNSSTrust'
assert result.kw.get('key') == 'ocspSigningCert cert-pki-ca'
assert result.kw.get('nickname') == 'ocspSigningCert cert-pki-ca'
assert result.kw.get('dbdir') == paths.PKI_TOMCAT_ALIAS_DIR
assert len(self.results) == 4
@patch('ipaserver.install.certs.CertDB')
def test_trust_bad(self, mock_certdb):
"""Test multiple unexpected trust flags"""
trust = {
'ocspSigningCert cert-pki-ca': 'u,u,u',
'subsystemCert cert-pki-ca': 'X,u,u',
'auditSigningCert cert-pki-ca': 'u,u,Pu',
'Server-Cert cert-pki-ca': 'X,u,u'
}
mock_certdb.return_value = mock_CertDB(trust)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertNSSTrust(registry)
self.results = capture_results(f)
result = self.results.results[1]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertNSSTrust'
assert result.kw.get('key') == 'subsystemCert cert-pki-ca'
assert result.kw.get('got') == 'X,u,u'
assert result.kw.get('expected') == 'u,u,u'
result = self.results.results[3]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertNSSTrust'
assert result.kw.get('key') == 'Server-Cert cert-pki-ca'
assert result.kw.get('got') == 'X,u,u'
assert result.kw.get('expected') == 'u,u,u'
assert len(self.results) == 4
@patch('ipaserver.install.cainstance.CAInstance')
def test_trust_caless(self, mock_cainstance):
"""Nothing to check if the master is CALess"""
mock_cainstance.return_value = CAInstance(False)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertNSSTrust(registry)
self.results = capture_results(f)
assert len(self.results) == 0
freeipa-healthcheck-0.10/tests/test_ipa_nssvalidation.py 0000664 0000000 0000000 00000007262 14200534377 0023604 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import Mock, patch
from util import capture_results, CAInstance
from ipapython.ipautil import _RunResult
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPANSSChainValidation
class DsInstance:
def get_server_cert_nickname(self, serverid):
return 'Server-Cert'
class TestNSSValidation(BaseTest):
patches = {
'ipahealthcheck.ipa.certs.get_dogtag_cert_password':
Mock(return_value='foo'),
'ipaserver.install.dsinstance.DsInstance':
Mock(return_value=DsInstance()),
}
@patch('ipaserver.install.cainstance.CAInstance')
@patch('ipapython.ipautil.run')
def test_nss_validation_ok(self, mock_run, mock_cainstance):
def run(args, raiseonerr=True):
result = _RunResult('', '', 0)
result.raw_output = b'certutil: certificate is valid\n'
result.raw_error_output = b''
return result
mock_run.side_effect = run
mock_cainstance.return_value = CAInstance()
framework = object()
registry.initialize(framework, config.Config)
f = IPANSSChainValidation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPANSSChainValidation'
@patch('ipaserver.install.cainstance.CAInstance')
@patch('ipapython.ipautil.run')
def test_nss_validation_bad(self, mock_run, mock_cainstance):
def run(args, raiseonerr=True):
result = _RunResult('', '', 255)
result.raw_output = str.encode(
'certutil: certificate is invalid: Peer\'s certificate issuer '
'has been marked as not trusted by the user.'
)
result.raw_error_output = b''
result.error_log = ''
return result
mock_run.side_effect = run
mock_cainstance.return_value = CAInstance()
framework = object()
registry.initialize(framework, config.Config)
f = IPANSSChainValidation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPANSSChainValidation'
@patch('ipaserver.install.cainstance.CAInstance')
@patch('ipapython.ipautil.run')
def test_nss_validation_ok_no_ca(self, mock_run, mock_cainstance):
"""Test with the CA marked as not configured so there should only
be a DS certificate to check.
"""
def run(args, raiseonerr=True):
result = _RunResult('', '', 0)
result.raw_output = b'certutil: certificate is valid\n'
result.raw_error_output = b''
return result
mock_run.side_effect = run
mock_cainstance.return_value = CAInstance(False)
framework = object()
registry.initialize(framework, config.Config)
f = IPANSSChainValidation(registry)
self.results = capture_results(f)
assert len(self.results) == 1
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPANSSChainValidation'
assert 'slapd-' in result.kw.get('key')
freeipa-healthcheck-0.10/tests/test_ipa_opensslvalidation.py 0000664 0000000 0000000 00000004603 14200534377 0024460 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import Mock, patch
from util import capture_results, CAInstance
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPAOpenSSLChainValidation
from ipapython.ipautil import _RunResult
class TestOpenSSLValidation(BaseTest):
patches = {
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
}
@patch('ipapython.ipautil.run')
def test_openssl_validation_ok(self, mock_run):
def run(args, raiseonerr=True):
result = _RunResult('', '', 0)
result.raw_output = bytes(
'{}: OK'.format(args[-1]).encode('utf-8'))
result.raw_error_output = b''
return result
mock_run.side_effect = run
framework = object()
registry.initialize(framework, config.Config)
f = IPAOpenSSLChainValidation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPAOpenSSLChainValidation'
@patch('ipapython.ipautil.run')
def test_openssl_validation_bad(self, mock_run):
def run(args, raiseonerr=True):
result = _RunResult('', '', 2)
result.raw_output = bytes(
'O = EXAMPLE.TEST, CN = ipa.example.test\n'
'error 20 at 0 depth lookup: unable to get local issuer '
'certificate\nerror {}: verification failed'.format(args[-1])
.encode('utf-8'))
result.raw_error_output = b''
result.error_log = ''
return result
mock_run.side_effect = run
framework = object()
registry.initialize(framework, config.Config)
f = IPAOpenSSLChainValidation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPAOpenSSLChainValidation'
assert 'failed' in result.kw.get('msg')
freeipa-healthcheck-0.10/tests/test_ipa_proxy.py 0000664 0000000 0000000 00000020660 14200534377 0022104 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from io import BytesIO
from unittest.mock import patch
from util import capture_results
import lxml.etree
from ipahealthcheck.core import config, constants
from ipahealthcheck.meta.plugin import registry
from ipahealthcheck.ipa.proxy import IPAProxySecretCheck
# Pre-parse the XML to avoid Mock weirdness
good_xml_input = """
""" # noqa: E501
good_xml = lxml.etree.parse(BytesIO(good_xml_input.encode('utf-8')))
good_ipa_proxy = """
SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
SSLVerifyClient none
ProxyPassMatch ajp://localhost:8009 secret=somesecret
ProxyPassReverse ajp://localhost:8009
"""
empty_ipa_proxy = """
"""
different_secrets_ipa_proxy = """
SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
SSLVerifyClient none
ProxyPassMatch ajp://localhost:8009 secret=somesecret
ProxyPassReverse ajp://localhost:8009
SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
SSLVerifyClient none
ProxyPassMatch ajp://localhost:8009 secret=othersecret
ProxyPassReverse ajp://localhost:8009
"""
# server.xml secret won't match Apache secret
mismatch1_xml_input = """
""" # noqa: E501
mismatch1_xml = lxml.etree.parse(BytesIO(mismatch1_xml_input.encode('utf-8')))
both_secrets_xml_input = """
""" # noqa: E501
both_secrets_xml = lxml.etree.parse(
BytesIO(both_secrets_xml_input.encode('utf-8'))
)
both_secrets_mismatch_xml_input = """
""" # noqa: E501
both_secrets_mismatch_xml = lxml.etree.parse(
BytesIO(both_secrets_mismatch_xml_input.encode('utf-8'))
)
empty_xml_input = """
"""
empty_xml = lxml.etree.parse(BytesIO(empty_xml_input.encode('utf-8')))
class TestIPAProxySecretCheck(BaseTest):
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_matching_secrets(self, mock_proxy, mock_ltree):
"""The passwords match"""
mock_ltree.return_value = good_xml
mock_proxy.return_value = good_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_xml_both_secrets(self, mock_proxy, mock_ltree):
"""server.xml defines both secret types and they match"""
mock_ltree.return_value = both_secrets_xml
mock_proxy.return_value = good_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_xml_both_secret_type_mismatch(self, mock_proxy, mock_ltree):
"""XML has both secret attributes and they do not match"""
mock_ltree.return_value = both_secrets_mismatch_xml
mock_proxy.return_value = good_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.kw.get('msg') == 'The AJP secrets in {server_xml} do '\
'not match'
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_xml_secret_mismatch(self, mock_proxy, mock_ltree):
"""The Apache secret doesn't match the tomcat secret"""
mock_ltree.return_value = mismatch1_xml
mock_proxy.return_value = good_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.kw.get('msg') == 'A ProxyPassMatch secret not found ' \
'in {server_xml}'
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_xml_no_connectors(self, mock_proxy, mock_ltree):
"""No connectors found in server.xml"""
mock_ltree.return_value = empty_xml
mock_proxy.return_value = good_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.kw.get('msg') == 'No AJP/1.3 Connectors defined in ' \
'{server_xml}'
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_no_proxypassmatch(self, mock_proxy, mock_ltree):
"""No connectors found in server.xml"""
mock_ltree.return_value = good_xml
mock_proxy.return_value = empty_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.kw.get('msg') == 'No ProxyPassMatch secrets found ' \
'in {proxy_conf}'
@patch('lxml.etree.parse')
@patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy')
def test_proxypassmatch_different_secrets(self, mock_proxy, mock_ltree):
"""No connectors found in server.xml"""
mock_ltree.return_value = good_xml
mock_proxy.return_value = different_secrets_ipa_proxy.split('\n')
framework = object()
registry.initialize(framework, config.Config())
f = IPAProxySecretCheck(registry)
self.results = capture_results(f)
print(self.results.results)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.CRITICAL
assert result.kw.get('msg') == 'Not all ProxyPassMatch secrets ' \
'match in {proxy_conf}'
result = self.results.results[1]
assert result.result == constants.CRITICAL
assert result.kw.get('msg') == 'A ProxyPassMatch secret not found ' \
'in {server_xml}'
freeipa-healthcheck-0.10/tests/test_ipa_revocation.py 0000664 0000000 0000000 00000006055 14200534377 0023076 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results, CAInstance
from util import m_api
from base import BaseTest
from unittest.mock import Mock
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertRevocation
from mock_certmonger import create_mock_dbus, _certmonger
from mock_certmonger import get_expected_requests, set_requests
class IPACertificate:
def __init__(self, serial_number=1):
self.subject = 'CN=RA AGENT'
self.issuer = 'CN=ISSUER'
self.serial_number = serial_number
class TestRevocation(BaseTest):
patches = {
'ipaserver.install.certs.is_ipa_issued_cert':
Mock(return_value=True),
'ipalib.x509.load_certificate_from_file':
Mock(return_value=IPACertificate()),
'ipahealthcheck.ipa.certs.get_expected_requests':
Mock(return_value=get_expected_requests()),
'ipalib.install.certmonger._cm_dbus_object':
Mock(side_effect=create_mock_dbus),
'ipalib.install.certmonger._certmonger':
Mock(return_value=_certmonger()),
'ipaserver.install.cainstance.CAInstance':
Mock(return_value=CAInstance()),
}
def test_revocation_ok(self):
m_api.Command.cert_show.side_effect = [
{
u'result': {
u"revoked": False,
}
},
{
u'result': {
u"revoked": False,
}
},
]
set_requests()
framework = object()
registry.initialize(framework, config.Config)
f = IPACertRevocation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertRevocation'
def test_revocation_one_bad(self):
m_api.Command.cert_show.side_effect = [
{
u'result': {
u"revoked": False,
}
},
{
u'result': {
u"revoked": True,
u"revocation_reason": 4,
}
},
]
set_requests()
framework = object()
registry.initialize(framework, config.Config)
f = IPACertRevocation(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertRevocation'
result = self.results.results[1]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertRevocation'
assert result.kw.get('revocation_reason') == 'superseded'
freeipa-healthcheck-0.10/tests/test_ipa_roles.py 0000664 0000000 0000000 00000007753 14200534377 0022057 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from unittest.mock import patch
from util import capture_results, CAInstance
from util import m_api
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.roles import (IPACRLManagerCheck,
IPARenewalMasterCheck)
class TestCRLManagerRole(BaseTest):
@patch('ipaserver.install.cainstance.CAInstance')
def test_not_crlmanager(self, mock_ca):
mock_ca.return_value = CAInstance(crlgen=False)
framework = object()
registry.initialize(framework, config.Config)
f = IPACRLManagerCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.roles'
assert result.check == 'IPACRLManagerCheck'
assert result.kw.get('crlgen_enabled') is False
@patch('ipaserver.install.cainstance.CAInstance')
def test_crlmanager(self, mock_ca):
mock_ca.return_value = CAInstance()
framework = object()
registry.initialize(framework, config.Config)
f = IPACRLManagerCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.roles'
assert result.check == 'IPACRLManagerCheck'
assert result.kw.get('crlgen_enabled') is True
@patch('ipaserver.install.cainstance.CAInstance')
def test_crlmanager_no_ca(self, mock_ca):
"""There should be no CRLManagerCheck without a CA"""
mock_ca.return_value = CAInstance(False)
framework = object()
registry.initialize(framework, config.Config)
f = IPACRLManagerCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 0
class TestRenewalMaster(BaseTest):
def test_renewal_master_not_set(self):
framework = object()
registry.initialize(framework, config.Config)
f = IPARenewalMasterCheck(registry)
m_api.Command.config_show.side_effect = [{
'result': {
}
}]
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.roles'
assert result.check == 'IPARenewalMasterCheck'
assert result.kw.get('master') is False
def test_not_renewal_master(self):
framework = object()
registry.initialize(framework, config.Config)
f = IPARenewalMasterCheck(registry)
m_api.Command.config_show.side_effect = [{
'result': {
'ca_renewal_master_server': 'something.ipa.example'
}
}]
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.roles'
assert result.check == 'IPARenewalMasterCheck'
assert result.kw.get('master') is False
def test_is_renewal_master(self):
framework = object()
registry.initialize(framework, config.Config)
f = IPARenewalMasterCheck(registry)
m_api.Command.config_show.side_effect = [{
'result': {
'ca_renewal_master_server': 'server.ipa.example'
}
}]
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.roles'
assert result.check == 'IPARenewalMasterCheck'
assert result.kw.get('master') is True
freeipa-healthcheck-0.10/tests/test_ipa_topology.py 0000664 0000000 0000000 00000016534 14200534377 0022604 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results
from util import m_api
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.topology import IPATopologyDomainCheck
class TestTopology(BaseTest):
def test_topology_ok(self):
m_api.Command.topologysuffix_verify.side_effect = [
{
u'result': {
u"in_order": True,
}
},
{
u'result': {
u"in_order": True,
}
},
]
m_api.Command.ca_is_enabled.return_value = {'result': True}
framework = object()
registry.initialize(framework, config.Config)
f = IPATopologyDomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.topology'
assert result.check == 'IPATopologyDomainCheck'
def test_topology_domain_bad(self):
m_api.Command.topologysuffix_verify.side_effect = [
{
u'result': {
u"connect_errors": [
[
u"ipa.example.test",
[u"ipa.example.test"],
[u"replica2.example.test"]
],
[
u"replica2.example.test",
[u"replica2.example.test"],
[u"ipa.example.test"]
]
],
u"in_order": False,
u"max_agmts": 4,
u"max_agmts_errors": []
}
},
{
u'result': {
u"in_order": True,
}
},
]
m_api.Command.ca_is_enabled.return_value = {'result': True}
framework = object()
registry.initialize(framework, config.Config)
f = IPATopologyDomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 3
for result in self.results.results:
assert result.source == 'ipahealthcheck.ipa.topology'
assert result.check == 'IPATopologyDomainCheck'
# The first two results are failures in the domain suffix, the
# third is a success in the ca suffix.
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'ipa.example.test'
assert result.kw.get('replicas') == ['replica2.example.test']
assert result.kw.get('suffix') == 'domain'
assert result.kw.get('type') == 'connect'
assert 'can\'t contact servers' in result.kw.get('msg')
result = self.results.results[1]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'replica2.example.test'
assert result.kw.get('replicas') == ['ipa.example.test']
assert result.kw.get('suffix') == 'domain'
assert result.kw.get('type') == 'connect'
assert 'can\'t contact servers' in result.kw.get('msg')
result = self.results.results[2]
assert result.result == constants.SUCCESS
assert result.kw.get('suffix') == 'ca'
def test_topology_ca_bad(self):
m_api.Command.topologysuffix_verify.side_effect = [
{
u'result': {
u"in_order": True,
}
},
{
u'result': {
u"connect_errors": [
[
u"ipa.example.test",
[u"ipa.example.test"],
[u"replica2.example.test"]
],
[
u"replica2.example.test",
[u"replica2.example.test"],
[u"ipa.example.test"]
]
],
u"in_order": False,
u"max_agmts": 4,
u"max_agmts_errors": []
}
},
]
m_api.Command.ca_is_enabled.return_value = {'result': True}
framework = object()
registry.initialize(framework, config.Config)
f = IPATopologyDomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 3
for result in self.results.results:
assert result.source == 'ipahealthcheck.ipa.topology'
assert result.check == 'IPATopologyDomainCheck'
# The first result is ok (domain) and the last two are failures
# (ca)
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.kw.get('suffix') == 'domain'
result = self.results.results[1]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'ipa.example.test'
assert result.kw.get('replicas') == ['replica2.example.test']
assert result.kw.get('suffix') == 'ca'
assert result.kw.get('type') == 'connect'
assert 'can\'t contact servers' in result.kw.get('msg')
result = self.results.results[2]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'replica2.example.test'
assert result.kw.get('replicas') == ['ipa.example.test']
assert result.kw.get('suffix') == 'ca'
assert result.kw.get('type') == 'connect'
assert 'can\'t contact servers' in result.kw.get('msg')
def test_topology_domain_max_agmts(self):
m_api.Command.topologysuffix_verify.side_effect = [
{
u'result': {
u"connect_errors": [],
u"in_order": False,
u"max_agmts": 1,
u"max_agmts_errors": [
[
u"ipa.example.test",
[u"replica2.example.test"],
],
],
}
},
{
u'result': {
u"in_order": True,
}
},
]
m_api.Command.ca_is_enabled.return_value = {'result': True}
framework = object()
registry.initialize(framework, config.Config)
f = IPATopologyDomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
for result in self.results.results:
assert result.source == 'ipahealthcheck.ipa.topology'
assert result.check == 'IPATopologyDomainCheck'
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.kw.get('key') == 'ipa.example.test'
assert result.kw.get('replicas') == ['replica2.example.test']
assert result.kw.get('suffix') == 'domain'
assert result.kw.get('type') == 'max'
assert 'recommended max' in result.kw.get('msg')
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.kw.get('suffix') == 'ca'
freeipa-healthcheck-0.10/tests/test_ipa_tracking.py 0000664 0000000 0000000 00000005401 14200534377 0022521 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results
from base import BaseTest
from ipahealthcheck.core import constants, config
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertTracking
from unittest.mock import Mock
from mock_certmonger import create_mock_dbus, _certmonger
from mock_certmonger import get_expected_requests, set_requests
class TestTracking(BaseTest):
patches = {
'ipahealthcheck.ipa.certs.get_expected_requests':
Mock(return_value=get_expected_requests()),
'ipalib.install.certmonger._cm_dbus_object':
Mock(side_effect=create_mock_dbus),
'ipalib.install.certmonger._certmonger':
Mock(return_value=_certmonger())
}
def test_known_cert_tracking(self):
set_requests()
framework = object()
registry.initialize(framework, config.Config)
f = IPACertTracking(registry)
self.results = capture_results(f)
assert len(self.results) == 2
def test_missing_cert_tracking(self):
# remove one of the requests to force it to be missing
set_requests(remove=0)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertTracking(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertTracking'
assert result.kw.get('key') == \
"cert-file=/var/lib/ipa/ra-agent.pem, " \
"key-file=/var/lib/ipa/ra-agent.key, " \
"ca-name=dogtag-ipa-ca-renew-agent, " \
"template_profile=caSubsystemCert, " \
"cert-storage=FILE, "\
"cert-presave-command=" \
"/usr/libexec/ipa/certmonger/renew_ra_cert_pre, " \
"cert-postsave-command=" \
"/usr/libexec/ipa/certmonger/renew_ra_cert"
def test_unknown_cert_tracking(self):
# Add a custom, unknown request
unknown = {
'nickname': '7777',
'cert-file': '/tmp/test.crt',
'key-file': '/tmp/test.key',
'ca-name': 'IPA',
}
set_requests(add=unknown)
framework = object()
registry.initialize(framework, config.Config)
f = IPACertTracking(registry)
self.results = capture_results(f)
assert len(self.results) == 3
result = self.results.results[2]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.certs'
assert result.check == 'IPACertTracking'
assert result.kw.get('key') == '7777'
freeipa-healthcheck-0.10/tests/test_ipa_trust.py 0000664 0000000 0000000 00000113317 14200534377 0022106 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import sys
from base import BaseTest
from collections import namedtuple
from unittest.mock import Mock, patch
from util import capture_results
from util import m_api
from ipahealthcheck.core import config, constants
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.trust import (IPATrustAgentCheck,
IPATrustDomainsCheck,
IPADomainCheck,
IPATrustCatalogCheck,
IPAsidgenpluginCheck,
IPATrustAgentMemberCheck,
IPATrustControllerPrincipalCheck,
IPATrustControllerServiceCheck,
IPATrustControllerGroupSIDCheck,
IPATrustControllerAdminSIDCheck,
IPATrustControllerConfCheck,
IPATrustPackageCheck)
from ipalib import errors
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, LDAPEntry
try:
from ipaserver.masters import ENABLED_SERVICE, HIDDEN_SERVICE
except ImportError:
from ipaserver.install.service import ENABLED_SERVICE, HIDDEN_SERVICE
try:
from ipapython.ipaldap import realm_to_serverid
except ImportError:
from ipaserver.install.installutils import realm_to_serverid
from ldap import OPT_X_SASL_SSF_MIN
from SSSDConfig import NoOptionError
class mock_ldap:
SCOPE_BASE = 1
SCOPE_ONELEVEL = 2
SCOPE_SUBTREE = 4
def __init__(self, ldapentry):
"""Initialize the results that we will return from get_entries"""
self.results = ldapentry
# def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None,
# attrs_list=None, get_effective_rights=False, **kwargs):
# if self.results is None:
# raise errors.NotFound(reason='test')
# return self.results
def get_entry(self, dn, attrs_list=None, time_limit=None,
size_limit=None, get_effective_rights=False):
if self.results is None:
raise errors.NotFound(reason='test')
return self.results
class mock_ldap_conn:
def set_option(self, option, invalue):
pass
def get_option(self, option):
if option == OPT_X_SASL_SSF_MIN:
return 256
return None
def search_s(self, base, scope, filterstr=None,
attrlist=None, attrsonly=0):
return tuple()
#
# Construct a setup with two direct trusts and one sub domain
#
def trust_find():
return [{
'result': [
{
'cn': ['ad.example'],
},
{
'cn': ['child.example'],
},
]
}]
def trustdomain_find():
return [
{
"result": [
{
"cn": ["ad.example"],
"ipantflatname": ["ADROOT"],
"ipanttrusteddomainsid": ["S-1-5-21-abc"],
"ipanttrusttype": ["2"],
"ipanttrustattributes": ["8"],
},
{
"cn": ["child.ad.example"],
"ipantflatname": ["CHILD.ADROOT"],
"ipanttrusteddomainsid": ["S-1-5-22-def"],
"ipanttrusttype": ["2"],
"ipanttrustattributes": ["1"],
},
],
},
{
"result": [
{
"cn": ["child.example"],
"ipantflatname": ["CHILD"],
"ipanttrusteddomainsid": ["S-1-5-21-ghi"],
"ipanttrusttype": ["2"],
"ipanttrustattributes": ["8"],
},
],
},
]
class SSSDDomain:
def __init__(self, return_ipa_server_mode=True, provider='ipa'):
self.return_ipa_server_mode = return_ipa_server_mode
self.provider = provider
def get_option(self, option):
if option in ('id_provider', 'auth_provider', 'chpass_provider',
'access_provider'):
return self.provider
if option == 'ipa_server_mode':
if self.return_ipa_server_mode is None:
raise NoOptionError()
return self.return_ipa_server_mode
return None
class SSSDConfig():
def __init__(self, return_domains=True, return_ipa_server_mode=True,
provider='ipa'):
"""
Knobs to control what data the configuration returns.
"""
self.return_domains = return_domains
self.return_ipa_server_mode = return_ipa_server_mode
self.provider = provider
def import_config(self):
pass
def list_active_domains(self):
return ('ipa.example',)
def get_domain(self, name):
return SSSDDomain(self.return_ipa_server_mode, self.provider)
class TestTrustAgent(BaseTest):
def test_no_trust_agent(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = False
f = IPATrustAgentCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
@patch('SSSDConfig.SSSDConfig')
def test_trust_agent_ok(self, mock_sssd):
mock_sssd.return_value = SSSDConfig(return_domains=True,
return_ipa_server_mode=True)
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustAgentCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustAgentCheck'
@patch('SSSDConfig.SSSDConfig')
def test_trust_agent_not_ipa(self, mock_sssd):
mock_sssd.return_value = SSSDConfig(return_domains=True,
return_ipa_server_mode=False)
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustAgentCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustAgentCheck'
assert result.kw.get('key') == 'ipa_server_mode_false'
assert result.kw.get('domain') == 'ipa.example'
@patch('SSSDConfig.SSSDConfig')
def test_trust_agent_fail(self, mock_sssd):
mock_sssd.return_value = SSSDConfig(return_domains=True,
return_ipa_server_mode=None)
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustAgentCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustAgentCheck'
assert result.kw.get('key') == 'ipa_server_mode_missing'
assert result.kw.get('domain') == 'ipa.example'
class TestTrustDomains(BaseTest):
def test_no_trust_agent(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = False
f = IPATrustDomainsCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
@patch('ipapython.ipautil.run')
def test_trust_domain_list_fail(self, mock_run):
run_result = namedtuple('run', ['returncode', 'error_log'])
run_result.returncode = 1
run_result.error_log = 'error'
mock_run.return_value = run_result
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustDomainsCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain_list_error'
@patch('ipapython.ipautil.run')
@patch('ipahealthcheck.ipa.trust.get_trust_domains')
def test_trust_get_trust_domains_fail(self, mock_trust, mock_run):
# sssctl domain-list
run_result = namedtuple('run', ['returncode', 'error_log'])
run_result.returncode = 0
run_result.error_log = ''
run_result.output = 'implicit_files\nipa.example\nad.example\n'
mock_run.return_value = run_result
mock_trust.side_effect = errors.NotFound(reason='bad')
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustDomainsCheck(registry)
self.results = capture_results(f)
# There are more than one result I just care about this particular
# value. The error is not fatal.
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'trust-find'
@patch('ipapython.ipautil.run')
def test_trust_get_trust_domains_ok(self, mock_run):
# sssctl domain-list
dlresult = namedtuple('run', ['returncode', 'error_log'])
dlresult.returncode = 0
dlresult.error_log = ''
dlresult.output = 'implicit_files\nipa.example\nad.example\n' \
'child.ad.example\nchild.example\n'
olresult = namedtuple('run', ['returncode', 'error_log'])
olresult.returncode = 0
olresult.error_log = ''
olresult.output = 'Online status: Online\n\n'
mock_run.side_effect = [dlresult, olresult, olresult, olresult]
# get_trust_domains()
m_api.Command.trust_find.side_effect = trust_find()
m_api.Command.trustdomain_find.side_effect = trustdomain_find()
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustDomainsCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 4
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-list'
assert result.kw.get('trust_domains') == \
'ad.example, child.ad.example, child.example'
assert result.kw.get('sssd_domains') == \
'ad.example, child.ad.example, child.example'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-status'
assert result.kw.get('domain') == 'ad.example'
result = self.results.results[2]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-status'
assert result.kw.get('domain') == 'child.ad.example'
result = self.results.results[3]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-status'
assert result.kw.get('domain') == 'child.example'
@patch('ipapython.ipautil.run')
def test_trust_get_trust_domains_mismatch(self, mock_run):
# sssctl domain-list
dlresult = namedtuple('run', ['returncode', 'error_log'])
dlresult.returncode = 0
dlresult.error_log = ''
dlresult.output = 'implicit_files\nipa.example\n' \
'child.example\n'
olresult = namedtuple('run', ['returncode', 'error_log'])
olresult.returncode = 0
olresult.error_log = ''
olresult.output = 'Online status: Online\n\n'
mock_run.side_effect = [dlresult, olresult, olresult]
# get_trust_domains()
m_api.Command.trust_find.side_effect = trust_find()
m_api.Command.trustdomain_find.side_effect = trustdomain_find()
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustDomainsCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-list'
assert result.kw.get('trust_domains') == \
'ad.example, child.ad.example, child.example'
assert result.kw.get('sssd_domains') == 'child.example'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustDomainsCheck'
assert result.kw.get('key') == 'domain-status'
assert result.kw.get('domain') == 'child.example'
class TestIPADomain(BaseTest):
@patch('SSSDConfig.SSSDConfig')
def test_ipa_domain_ok(self, mock_sssd):
mock_sssd.return_value = SSSDConfig(provider='ipa')
framework = object()
registry.initialize(framework, config.Config)
# being a trust agent isn't mandatory, test without
registry.trust_agent = False
f = IPADomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPADomainCheck'
@patch('SSSDConfig.SSSDConfig')
def test_ipa_domain_ad(self, mock_sssd):
mock_sssd.return_value = SSSDConfig(provider='ad')
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPADomainCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 4
for result in self.results.results:
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPADomainCheck'
assert result.kw.get('provider') == 'ad'
class TestTrustCatalog(BaseTest):
def test_no_trust_agent(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = False
f = IPATrustCatalogCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
@patch('pysss_nss_idmap.getnamebysid')
@patch('ipapython.ipautil.run')
def test_trust_catalog_ok(self, mock_run, mock_getnamebysid):
# id Administrator@ad.example
dsresult = namedtuple('run', ['returncode', 'error_log'])
dsresult.returncode = 0
dsresult.error_log = ''
dsresult.output = 'Active servers:\nAD Global Catalog: ' \
'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \
'IPA: master.ipa.vm\n\n'
ds2result = namedtuple('run', ['returncode', 'error_log'])
ds2result.returncode = 0
ds2result.error_log = ''
ds2result.output = 'Active servers:\nAD Global Catalog: ' \
'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \
mock_run.side_effect = [dsresult, dsresult, ds2result]
mock_getnamebysid.side_effect = [
{'S-1-5-21-abc-500': {'name': 'admin@ad.example', 'type': 3}},
{'S-1-5-21-ghi-500': {'name': 'admin@child.ad.example', 'type': 3}},
{'S-1-5-21-def-500': {'name': 'admin@child.example', 'type': 3}}
]
# get_trust_domains()
m_api.Command.trust_find.side_effect = trust_find()
m_api.Command.trustdomain_find.side_effect = trustdomain_find()
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustCatalogCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 9
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'Domain Security Identifier'
assert result.kw.get('sid') == 'S-1-5-21-abc'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Global Catalog'
assert result.kw.get('domain') == 'ad.example'
result = self.results.results[2]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Domain Controller'
assert result.kw.get('domain') == 'ad.example'
result = self.results.results[3]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'Domain Security Identifier'
assert result.kw.get('sid') == 'S-1-5-22-def'
result = self.results.results[4]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Global Catalog'
assert result.kw.get('domain') == 'child.ad.example'
result = self.results.results[5]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Domain Controller'
result = self.results.results[6]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'Domain Security Identifier'
assert result.kw.get('sid') == 'S-1-5-21-ghi'
result = self.results.results[7]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Global Catalog'
assert result.kw.get('domain') == 'child.example'
result = self.results.results[8]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustCatalogCheck'
assert result.kw.get('key') == 'AD Domain Controller'
assert result.kw.get('domain') == 'child.example'
class Testsidgen(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_no_trust_agent(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = False
f = IPAsidgenpluginCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_sidgen_ok(self):
attrs = {
'nsslapd-pluginEnabled': ['on'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('cn=plugin, cn=config'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPAsidgenpluginCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPAsidgenpluginCheck'
assert result.kw.get('key') == 'IPA SIDGEN'
result = self.results.results[1]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPAsidgenpluginCheck'
assert result.kw.get('key') == 'ipa-sidgen-task'
def test_sidgen_fail(self):
attrs = {
'nsslapd-pluginEnabled': ['off'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, DN('cn=plugin, cn=config'))
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPAsidgenpluginCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 2
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPAsidgenpluginCheck'
assert result.kw.get('key') == 'IPA SIDGEN'
result = self.results.results[1]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPAsidgenpluginCheck'
assert result.kw.get('key') == 'ipa-sidgen-task'
class TestTrustAgentMember(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_no_trust_agent(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = False
f = IPATrustAgentMemberCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_member_ok(self):
agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host,
m_api.env.basedn)
group_dn = DN(('cn', 'adtrust agents'),
m_api.env.container_sysaccounts,
m_api.env.basedn)
attrs = {
'memberof': [group_dn],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, agent_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustAgentMemberCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustAgentMemberCheck'
assert result.kw.get('key') == m_api.env.host
def test_member_fail(self):
agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host,
m_api.env.basedn)
attrs = {
'memberof': [agent_dn],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, agent_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_agent = True
f = IPATrustAgentMemberCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustAgentMemberCheck'
assert result.kw.get('key') == m_api.env.host
class TestControllerPrincipal(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_not_trust_controller(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
f = IPATrustControllerPrincipalCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_principal_ok(self):
agent_dn = DN(('krbprincipalname',
'cifs/%s@%s' % (m_api.env.host, m_api.env.realm)),
m_api.env.container_service, m_api.env.basedn)
group_dn = DN(('cn', 'adtrust agents'),
m_api.env.container_sysaccounts,
m_api.env.basedn)
attrs = {
'memberof': [group_dn],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, agent_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerPrincipalCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerPrincipalCheck'
assert result.kw.get('key') == 'cifs/%s@%s' % \
(m_api.env.host, m_api.env.realm)
def test_member_fail(self):
agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host,
m_api.env.basedn)
attrs = {
'memberof': [agent_dn],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, agent_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerPrincipalCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.kw.get('key') == 'cifs/%s@%s' % \
(m_api.env.host, m_api.env.realm)
class TestControllerService(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_not_trust_controller(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
f = IPATrustControllerServiceCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_service_enabled(self):
service_dn = DN(('cn', 'ADTRUST'))
for type in [ENABLED_SERVICE, HIDDEN_SERVICE]:
attrs = {
'ipaconfigstring': [type],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, service_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerServiceCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerServiceCheck'
assert result.kw.get('key') == 'ADTRUST'
def test_principal_fail(self):
service_dn = DN(('cn', 'ADTRUST'))
attrs = {
'ipaconfigstring': ['disabledService'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, service_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerServiceCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.kw.get('key') == 'ADTRUST'
class TestControllerGroupSID(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_not_trust_controller(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
f = IPATrustControllerGroupSIDCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_principal_ok(self):
admins_dn = DN(('cn', 'admins'))
attrs = {
'ipantsecurityidentifier':
['S-1-5-21-1234-5678-1976041503-512'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, admins_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerGroupSIDCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerGroupSIDCheck'
assert result.kw.get('key') == 'ipantsecurityidentifier'
assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-512'
def test_principal_fail(self):
admins_dn = DN(('cn', 'admins'))
attrs = {
'ipantsecurityidentifier':
['S-1-5-21-1234-5678-1976041503-500'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, admins_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerGroupSIDCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerGroupSIDCheck'
assert result.kw.get('key') == 'ipantsecurityidentifier'
assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500'
class TestControllerAdminSID(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_not_trust_controller(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
f = IPATrustControllerAdminSIDCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
def test_principal_ok(self):
admin_dn = DN(('uid', 'admin'))
attrs = {
'ipantsecurityidentifier':
['S-1-5-21-1234-5678-1976041503-500'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, admin_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerAdminSIDCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerAdminSIDCheck'
assert result.kw.get('key') == 'ipantsecurityidentifier'
assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500'
def test_principal_fail(self):
admin_dn = DN(('uid', 'admin'))
attrs = {
'ipantsecurityidentifier':
['S-1-5-21-1234-5678-1976041503-400'],
}
fake_conn = LDAPClient('ldap://localhost', no_schema=True)
ldapentry = LDAPEntry(fake_conn, admin_dn)
for attr, values in attrs.items():
ldapentry[attr] = values
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerAdminSIDCheck(registry)
f.conn = mock_ldap(ldapentry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerAdminSIDCheck'
assert result.kw.get('key') == 'ipantsecurityidentifier'
assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-400'
class TestControllerConf(BaseTest):
patches = {
'ldap.initialize':
Mock(return_value=mock_ldap_conn()),
}
def test_not_trust_controller(self):
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
f = IPATrustControllerConfCheck(registry)
self.results = capture_results(f)
# Zero because the call was skipped altogether
assert len(self.results) == 0
@patch('ipapython.ipautil.run')
def test_ldapi_ok(self, mock_run):
ldapi_socket = "ipasam:ldapi://%%2fvar%%2frun%%2fslapd-%s.socket" % \
realm_to_serverid(m_api.env.realm)
run_result = namedtuple('run', ['returncode', 'output'])
run_result.returncode = 0
run_result.output = '[global]\n\tpassdb backend=%s' % ldapi_socket
mock_run.return_value = run_result
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = True
f = IPATrustControllerConfCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustControllerConfCheck'
assert result.kw.get('key') == 'net conf list'
class TestPackageCheck(BaseTest):
def test_agent_with_package(self):
# Note that this test assumes the import is installed
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
registry.trust_agent = True
f = IPATrustPackageCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustPackageCheck'
def test_agent_without_package(self):
# Note that this test assumes the import is installed
framework = object()
registry.initialize(framework, config.Config)
registry.trust_controller = False
registry.trust_agent = True
# Hose up the module so the import fails
save = sys.modules['ipaserver.install']
sys.modules['ipaserver.install'] = 'foo'
f = IPATrustPackageCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.WARNING
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustPackageCheck'
sys.modules['ipaserver.install'] = save
freeipa-healthcheck-0.10/tests/test_meta.py 0000664 0000000 0000000 00000020126 14200534377 0021015 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from base import BaseTest
from collections import namedtuple
from unittest.mock import patch
from util import capture_results
from ipahealthcheck.core import config, constants
from ipahealthcheck.meta.plugin import registry
from ipahealthcheck.meta.core import MetaCheck
from ipapython import ipautil
from ipaplatform.paths import paths
if 'FIPS_MODE_SETUP' not in dir(paths):
paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup'
def gen_result(returncode, output='', error=''):
"""
Generate the result of an execution.
Creates a run namespace and sets the output as provided.
"""
run_result = namedtuple(
'run', ['returncode', 'raw_output', 'output_log', 'error_log']
)
run_result.returncode = returncode
run_result.raw_output = output.encode('utf-8')
run_result.output_log = output
run_result.error_log = error
return run_result
class TestMetaFIPS(BaseTest):
@patch('os.path.exists')
def test_fips_no_fips_mode_setup(self, mock_exists):
mock_exists.return_value = False
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'missing %s' % paths.FIPS_MODE_SETUP
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_fips_disabled(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(2),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'disabled'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_fips_enabled(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(0),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'enabled'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_fips_inconsistent(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(1),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'inconsistent'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_fips_unknown(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(103),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'unknown'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_fips_failed(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
ipautil.CalledProcessError(
1, 'fips-mode-setup', output='execution failed'
),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.ERROR
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('fips') == 'failed to check'
class TestMetaACME(BaseTest):
@patch('os.path.exists')
def test_acme_no_ipa_acme_status(self, mock_exists):
mock_exists.return_value = False
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('acme') == \
'missing %s' % '/usr/sbin/ipa-acme-manage'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_acme_disabled(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(0),
gen_result(0, output='ACME is disabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('acme') == 'disabled'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_acme_enabled(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(0),
gen_result(0, output='ACME is enabled'),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('acme') == 'enabled'
@patch('os.path.exists')
@patch('ipapython.ipautil.run')
def test_acme_unknown(self, mock_run, mock_exists):
mock_exists.return_value = True
mock_run.side_effect = [
gen_result(0),
gen_result(
0,
error="cannot connect to 'https://somewhere/acme/login"
),
]
framework = object()
registry.initialize(framework, config.Config())
f = MetaCheck(registry)
self.results = capture_results(f)
assert len(self.results) == 1
result = self.results.results[0]
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.meta.core'
assert result.check == 'MetaCheck'
assert result.kw.get('acme') == 'unknown'
freeipa-healthcheck-0.10/tests/test_meta_services.py 0000664 0000000 0000000 00000001353 14200534377 0022721 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import capture_results
from base import BaseTest
from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.meta.services import httpd
from ipahealthcheck.core import config
class TestServices(BaseTest):
def test_simple_service(self):
"""
Test a service. It was chosen at random.
The purpose of this test is to exercise the service check
code path and not to confirm that a particular service is
running.
"""
framework = object()
registry.initialize(framework, config.Config)
f = httpd(registry)
self.results = capture_results(f)
assert len(self.results) == 1
freeipa-healthcheck-0.10/tests/test_options.py 0000664 0000000 0000000 00000002612 14200534377 0021562 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2022 FreeIPA Contributors see COPYING for license
#
import argparse
import os
import tempfile
from unittest.mock import patch
from ipahealthcheck.core.core import RunChecks
from ipahealthcheck.core.plugin import Results
options = argparse.Namespace(check=None, source=None, debug=False,
indent=2, list_sources=False,
output_type='json', output_file=None,
verbose=False, version=False, config=None)
@patch('ipahealthcheck.core.core.run_service_plugins')
@patch('ipahealthcheck.core.core.run_plugins')
@patch('ipahealthcheck.core.core.parse_options')
def test_options_merge(mock_parse, mock_run, mock_service):
"""
Test merging file-based and CLI options
"""
mock_service.return_value = (Results(), [])
mock_run.return_value = Results()
mock_parse.return_value = options
fd, config_path = tempfile.mkstemp()
os.close(fd)
with open(config_path, "w") as fd:
fd.write('[default]\n')
fd.write('output_type=human\n')
fd.write('indent=5\n')
try:
run = RunChecks(['ipahealthcheck.registry'], config_path)
run.run_healthcheck()
# verify two valus that have defaults with our overriden values
assert run.options.output_type == 'human'
assert run.options.indent == '5'
finally:
os.remove(config_path)
freeipa-healthcheck-0.10/tests/test_plugins.py 0000664 0000000 0000000 00000001753 14200534377 0021555 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
import time
from ipahealthcheck.core.plugin import Plugin, Registry, Result
from ipahealthcheck.core.core import run_plugins
from ipahealthcheck.core import constants
def test_timeout():
"""
Test that timeouts are detected.
"""
class plugin1(Plugin):
def check(self):
time.sleep(5)
class plugin2(Plugin):
def check(self):
yield Result(self, constants.SUCCESS, key='test', msg='pass')
# Create a registry
r = Registry()
# Register the plugins
r(plugin1)
r(plugin2)
# Collect the results
results = run_plugins(r.get_plugins(), (), None, None, {}, timeout=1)
assert len(results.results) == 2
assert results.results[0].result == constants.ERROR
assert results.results[0].kw.get('exception') == 'Request timed out'
assert results.results[1].result == constants.SUCCESS
assert results.results[1].kw.get('msg') == 'pass'
freeipa-healthcheck-0.10/tests/test_registry.py 0000664 0000000 0000000 00000001444 14200534377 0021741 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import raises
from ipahealthcheck.core.plugin import Plugin, Registry
def test_Registry():
"""
Test the `ipahealthcheck.core.Registry` class
"""
class plugin1(Plugin):
pass
class plugin2(Plugin):
pass
# Create a registry
r = Registry()
# Check that TypeError is raised trying to register something that isn't
# a class:
p = plugin1(r)
e = raises(TypeError, r, p)
assert str(e) == 'plugin must be callable; got %r' % p
# Register the plugins
r(plugin1)
r(plugin2)
# TODO: enforce plugin uniqueness
# Test registration
names = [plugin.__class__.__name__ for plugin in r.get_plugins()]
assert(names == ['plugin1', 'plugin2'])
freeipa-healthcheck-0.10/tests/test_results.py 0000664 0000000 0000000 00000004363 14200534377 0021575 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from util import raises
from ipahealthcheck.core.plugin import Registry, Plugin, Result, Results
from ipahealthcheck.core import constants
def test_Result():
"""
Test the `ipahealthcheck.plugin.Result` class
"""
registry = Registry()
p = Plugin(registry)
# Standard case of passing plugin to Result
r = Result(p, constants.SUCCESS)
kw = dict(key='value')
r = Result(p, constants.SUCCESS, **kw)
e = raises(TypeError, Result)
assert "__init__() missing 2 required positional arguments: " \
"'plugin' and 'result'" in str(e)
# Test passing source and check to Result. This is used for loading
# a previous output.
try:
r = Result(None, constants.SUCCESS)
except TypeError as e:
assert str(e) == "source and check or plugin must be provided"
try:
r = Result(None, constants.SUCCESS, source='test')
except TypeError as e:
assert str(e) == "source and check or plugin must be provided"
try:
r = Result(None, constants.SUCCESS, check='test')
except TypeError as e:
assert str(e) == "source and check or plugin must be provided"
r = Result(None, constants.SUCCESS, source='test', check='test')
# Test results
r = Result(p, constants.SUCCESS)
results = Results()
results.add(r)
assert len(results) == 1
r = Result(p, constants.CRITICAL)
results2 = Results()
results2.add(r)
assert len(results2) == 1
results.extend(results2)
assert len(results) == 2
output = list(results.output())
assert len(output) == 2
for x in output:
assert x['source'] == 'ipahealthcheck.core.plugin'
assert x['check'] == 'Plugin'
assert x['result'] in (constants.getLevelName(constants.SUCCESS),
constants.getLevelName(constants.CRITICAL))
assert len(x['kw']) == 0
def test_getLevel():
assert constants.getLevel('SUCCESS') == constants.SUCCESS
assert constants.getLevel('WARNING') == constants.WARNING
assert constants.getLevel('ERROR') == constants.ERROR
assert constants.getLevel('CRITICAL') == constants.CRITICAL
assert constants.getLevel('FOO') == 'FOO'
freeipa-healthcheck-0.10/tests/test_suppress.py 0000664 0000000 0000000 00000011137 14200534377 0021755 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2022 FreeIPA Contributors see COPYING for license
#
import argparse
import os
import tempfile
from unittest.mock import patch
from ipahealthcheck.core import constants
from ipahealthcheck.core.core import RunChecks
from ipahealthcheck.core.plugin import Result, Results, Plugin, duration
from ipahealthcheck.system.plugin import Registry
from ipahealthcheck.core.output import output_registry, Output
options = argparse.Namespace(check=None, source=None, debug=False,
indent=2, list_sources=False,
output_type='suppresstest', output_file=None,
verbose=False, version=False, config=None)
outputdata = None
@output_registry
class SuppressTest(Output):
"""Test suppression"""
options = ()
def generate(self, data):
global outputdata
outputdata = data
class UserPlugin(Plugin):
pass
class UserRegistry(Registry):
def initialize(self, framework, config, options=None):
pass
registry = UserRegistry()
@registry
class PluginOne(UserPlugin):
@duration
def check(self):
yield Result(self, constants.ERROR, key="test1", msg="test1")
@registry
class PluginTwo(UserPlugin):
@duration
def check(self):
yield Result(self, constants.ERROR, key="test2", msg="test2")
@patch('ipahealthcheck.core.core.run_service_plugins')
@patch('ipahealthcheck.core.core.parse_options')
@patch('ipahealthcheck.core.core.find_registries')
def test_suppress_none(mock_find, mock_parse, mock_service):
"""
Test suppressing plugins
"""
global outputdata
mock_service.return_value = (Results(), [])
mock_parse.return_value = options
mock_find.return_value = {'test': registry}
outputdata = None
fd, config_path = tempfile.mkstemp()
os.close(fd)
with open(config_path, "w") as fd:
fd.write('[default]\n')
try:
run = RunChecks(['test'], config_path)
run.run_healthcheck()
assert len(outputdata) == 2
finally:
os.remove(config_path)
@patch('ipahealthcheck.core.core.run_service_plugins')
@patch('ipahealthcheck.core.core.parse_options')
@patch('ipahealthcheck.core.core.find_registries')
def test_suppress_source(mock_find, mock_parse, mock_service):
"""
Test suppressing plugins
"""
global outputdata
mock_service.return_value = (Results(), [])
mock_parse.return_value = options
mock_find.return_value = {'test': registry}
outputdata = None
fd, config_path = tempfile.mkstemp()
os.close(fd)
with open(config_path, "w") as fd:
fd.write('[default]\n')
fd.write('[excludes]\n')
fd.write('source=test_suppress\n')
try:
run = RunChecks(['test'], config_path)
run.run_healthcheck()
assert len(outputdata) == 0
finally:
os.remove(config_path)
@patch('ipahealthcheck.core.core.run_service_plugins')
@patch('ipahealthcheck.core.core.parse_options')
@patch('ipahealthcheck.core.core.find_registries')
def test_suppress_check(mock_find, mock_parse, mock_service):
"""
Test suppressing plugins
"""
global outputdata
mock_service.return_value = (Results(), [])
mock_parse.return_value = options
mock_find.return_value = {'test': registry}
outputdata = None
fd, config_path = tempfile.mkstemp()
os.close(fd)
with open(config_path, "w") as fd:
fd.write('[default]\n')
fd.write('[excludes]\n')
fd.write('check=PluginOne\n')
try:
run = RunChecks(['test'], config_path)
run.run_healthcheck()
assert len(outputdata) == 1
assert outputdata[0].get('check') == 'PluginTwo'
finally:
os.remove(config_path)
@patch('ipahealthcheck.core.core.run_service_plugins')
@patch('ipahealthcheck.core.core.parse_options')
@patch('ipahealthcheck.core.core.find_registries')
def test_suppress_key(mock_find, mock_parse, mock_service):
"""
Test suppressing plugins
"""
global outputdata
mock_service.return_value = (Results(), [])
mock_parse.return_value = options
mock_find.return_value = {'test': registry}
outputdata = None
fd, config_path = tempfile.mkstemp()
os.close(fd)
with open(config_path, "w") as fd:
fd.write('[default]\n')
fd.write('[excludes]\n')
fd.write('key=test2\n')
try:
run = RunChecks(['test'], config_path)
run.run_healthcheck()
assert len(outputdata) == 1
assert outputdata[0].get('check') == 'PluginOne'
assert outputdata[0].get('kw').get('msg') == 'test1'
finally:
os.remove(config_path)
freeipa-healthcheck-0.10/tests/test_system_filesystemspace.py 0000664 0000000 0000000 00000007176 14200534377 0024705 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from __future__ import division
from base import BaseTest
from unittest.mock import Mock
from util import capture_results
from collections import namedtuple
from ipahealthcheck.core import config, constants
from ipahealthcheck.system.plugin import registry
from ipahealthcheck.system.filesystemspace import FileSystemSpaceCheck
from ipahealthcheck.system.filesystemspace import in_container
class TestFileSystemNotEnoughFreeSpace(BaseTest):
usage = namedtuple('usage', ['total', 'used', 'free'])
usage.total = 2087428096
usage.used = 1628193914
usage.free = 459234182
patches = {
'shutil.disk_usage':
Mock(return_value=usage),
}
def test_filesystem_near_enospc(self):
framework = object()
registry.initialize(framework, config.Config)
f = FileSystemSpaceCheck(registry)
self.results = capture_results(f)
expected_results = 10 if in_container() else 12
count = 0
for result in self.results.results:
if result.result == constants.ERROR:
count += 1
assert result.source == 'ipahealthcheck.system.filesystemspace'
assert result.check == 'FileSystemSpaceCheck'
assert 'free space under threshold' in result.kw.get('msg')
else:
assert 'free space percentage within' in result.kw.get('msg')
assert len(self.results) == expected_results
assert count == expected_results / 2
class TestFileSystemNotEnoughFreeSpacePercentage(BaseTest):
usage = namedtuple('usage', ['total', 'used', 'free'])
usage.total = 10437140480
usage.used = 8913305600
usage.free = 1523834880
patches = {
'shutil.disk_usage':
Mock(return_value=usage),
}
def test_filesystem_risking_fragmentation(self):
framework = object()
registry.initialize(framework, config.Config)
f = FileSystemSpaceCheck(registry)
self.results = capture_results(f)
expected_results = 10 if in_container() else 12
count = 0
for result in self.results.results:
if result.result == constants.ERROR:
count += 1
assert result.source == 'ipahealthcheck.system.filesystemspace'
assert result.check == 'FileSystemSpaceCheck'
assert 'free space percentage under' in result.kw.get('msg')
else:
assert 'free space within limits' in result.kw.get('msg')
assert len(self.results) == expected_results
assert count == expected_results / 2
class TestFileSystemEnoughFreeSpace(BaseTest):
usage = namedtuple('usage', ['total', 'used', 'free'])
usage.total = 10437140480
usage.used = 1523834880
usage.free = 8913305600
patches = {
'shutil.disk_usage':
Mock(return_value=usage),
}
def test_filesystem_with_enough_space(self):
framework = object()
registry.initialize(framework, config.Config)
f = FileSystemSpaceCheck(registry)
self.results = capture_results(f)
expected_results = 10 if in_container() else 12
for result in self.results.results:
assert result.result == constants.SUCCESS
assert result.source == 'ipahealthcheck.system.filesystemspace'
assert result.check == 'FileSystemSpaceCheck'
assert (
'free space percentage within' in result.kw.get('msg') or
'free space within limits' in result.kw.get('msg')
)
assert len(self.results) == expected_results
freeipa-healthcheck-0.10/tests/util.py 0000664 0000000 0000000 00000007216 14200534377 0020012 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipahealthcheck.core.plugin import Results
from unittest.mock import patch, Mock
import ipalib
from ipapython.dn import DN
class ExceptionNotRaised(Exception):
"""
Exception raised when an *expected* exception is *not* raised during a
unit test.
"""
msg = 'expected %s'
def __init__(self, expected):
self.expected = expected
def __str__(self):
return self.msg % self.expected.__name__
def raises(exception, callback, *args, **kw):
"""
Tests that the expected exception is raised; raises ExceptionNotRaised
if test fails.
"""
try:
callback(*args, **kw)
except exception as e:
return e
raise ExceptionNotRaised(exception)
def capture_results(f):
"""
Loop over check() and collect the results.
"""
results = Results()
for result in f.check():
if result is not None:
results.add(result)
return results
class CAInstance:
"""A bare-bones CAinistance override
This is needed to control whether the underlying master is
CAless or CAful.
"""
def __init__(self, enabled=True, crlgen=True):
self.enabled = enabled
self.crlgen = crlgen
def is_configured(self):
return self.enabled
def is_crlgen_enabled(self):
return self.crlgen
class KRAInstance:
"""A bare-bones KRAinistance override
This is needed to control whether the underlying master is
has a KRA installed or not.
"""
def __init__(self, installed=True):
self.installed = installed
def is_installed(self):
return self.installed
class ServiceBasedRole:
"""A bare-bones role override
This is just enough to satisfy the initialization code so
the AD Trust status can be determined. It will always default
to false and the registry should be overridden directly in the
test cases.
"""
def __init__(self, attr_name=None, name=None, component_services=None):
pass
def status(self, api_instance, server=None, attrs_list=("*",)):
return [dict()]
class ADtrustBasedRole(ServiceBasedRole):
"""A bare-bones role override
This is just enough to satisfy the initialization code so
the AD Trust status can be determined. It will always default
to false and the registry should be overridden directly in the
test cases.
"""
def __init__(self, attr_name=None, name=None):
pass
# Mock api. This file needs to be imported before anything that would
# import ipalib.api in order for it to be replaced properly.
p_api = patch('ipalib.api', autospec=ipalib.api)
m_api = p_api.start()
m_api.isdone.return_value = True
m_api.env = Mock()
m_api.env.host = 'server.ipa.example'
m_api.env.server = 'server.ipa.example'
m_api.env.realm = u'IPA.EXAMPLE'
m_api.env.domain = u'ipa.example'
m_api.env.basedn = u'dc=ipa,dc=example'
m_api.env.container_user = DN(('cn', 'users'), ('cn', 'accounts'))
m_api.env.container_group = DN(('cn', 'groups'), ('cn', 'accounts'))
m_api.env.container_host = DN(('cn', 'computers'), ('cn', 'accounts'))
m_api.env.container_sysaccounts = DN(('cn', 'sysaccounts'), ('cn', 'etc'))
m_api.env.container_service = DN(('cn', 'services'), ('cn', 'accounts'))
m_api.env.container_masters = DN(('cn', 'masters'))
m_api.Backend = Mock()
m_api.Command = Mock()
m_api.Command.ping.return_value = {
u'summary': u'IPA server version 4.4.3. API version 2.215',
}
def no_exceptions(results):
"""Given Results ensure that an except was not raised"""
for result in results.results:
assert 'exception' not in result.kw
freeipa-healthcheck-0.10/tox.ini 0000664 0000000 0000000 00000001242 14200534377 0016625 0 ustar 00root root 0000000 0000000 [tox]
minversion=2.3.1
envlist=py3,flake8,pep8,pylint
[testenv]
# sitepackages is needed for ipalib but this confuses the deps for pytest
# pep8 and flake8 so those must be installed globally as well.
sitepackages=True
[testenv:py3]
basepython=python3
commands=
{envpython} -m pytest
[testenv:flake8]
basepython=python3
deps=flake8
commands=
{envpython} -m flake8 src/ipahealthcheck tests
[testenv:lint]
deps=pylint
setenv=
PYTHONPATH={env:PYTHONPATH:}{:}{toxinidir}
commands=
{envpython} -m pylint --rcfile=pylintrc --load-plugins=pylint_plugins src tests
[testenv:pep8]
deps=pycodestyle
commands=
{envpython} -m pycodestyle src/ipahealthcheck tests