Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
===============================
Community.MongoDB Release Notes
===============================
.. contents:: Topics
v1.4.2
=======
Release Summary
---------------
This release is a maintenance release.
Minor Changes
--------------
- 483 - Removes previous upper bound restriction for communiry.general collection,
- 483 - Use extended FQCN for pam_limits (community.general.system.pam_limits instead of community.general.pam_limits).
v1.4.1
=======
Release Summary
---------------
This release is a maintenance release.
Minor Changes
--------------
- 474 - Adds log_path parameter to mongodb_mongod, mongodb_mongos and mongodb_config roles.
Bugfixes
--------
- 479 - mongodb_shell - Correct supports_check_mode value. Used to be true, which is wrong, now false.
v1.4.0
=======
Release Summary
---------------
This release is a maintenance release.
Pymongo versions 3.12.* or 4.* are now required.
MongoDB version 4+ are also required but can be overriden if desired.
Major Changes
---------------
- 470 - Removes depreciated distutils package and require Pymongo 3.12+ and MongoDB 4+
Adds a new parameter strict_compatibility (default true).
Set to false to disable Pymongo and MongoDB requirements.
v1.3.4
=======
Release Summary
---------------
This release is a maintenance release.
Bug Fixes
---------
- 466 & 467 - Fixes localhost exception bug due to directConnection parameter in newer pymongo versions.
v1.3.3
=======
Release Summary
---------------
This release is a maintenance release.
Bug Fixes
---------
- 448 - Fix issue in roles where mongod does not restart when a custom bind_ip is set.
- 440 - Fix incorrect alias ssl_crlfile.
- 450 - Fix issues with mongodb_replicaset connecting with the pymongo 4.0.X driver.
Minor Changes
---------------
- 450 - mongodb_replicaset. Introduce cluster_cmd parameter. Can be set to isMaster or hello.
Hello is the default. isMaster is useful for older versions of MongoDB.
See [db.hello()](https://www.mongodb.com/docs/manual/reference/method/db.hello/) for more.
v1.3.2
=======
Release Summary
---------------
This release is a maintenance release.
Minor Changes
---------------
- 413 - mongodb_shell - Adds escape_param function that will work better across various python versions.
- 414-416 - Minor documentation improvements.
- 411 - mongodb_shell - FIx missing db parameter when fiel parmeter is used.
- 403 - Make db path configurable in roles.
- 401 - mongodb_replicaset - Add further examples.
- 399 - Removes unused imports from modules.
- 396 - Add tags to roles.
- 387 - Fix doucmentation for mongod cache plugin.
Major Changes
---------------
- 397 & 376 - mongodb_replicaset - Add reconfigure abilities to module. Add and removes members from replicasets.
v1.3.1
======
Release Summary
---------------
This release is a maintenance release. The GitHub CI has been updated to include MongoDB 5.0 as well
as a few new features. The mongosh shell is now supported in the mongodb_shell module. Support for the
old mongo shell will be removed in a future release.
Minor Changes
-------------
- 360 - mongodb_shell - Adds support for the mongosh shell now available with MongoDB 5.0.
- 368 - mongodb_shell - Use shlex escape function.
- 370 - mongodb_install - Adds mongodb_hold_packages variable. Runs the lock_mongodb_packages.sh script
to either lock mongodb-org packages at a specific version or to release the lock.
Set to "HOLD" or "NOHOLD" as desired. No checks are made to see if the hold already exists or not.
By default this variable is undefined and the script is not executed.
The task is executed at the end and it is possible that packages could be upgraded
before the lock is initially applied.
Deprecated Features
-------------------
- mongodb_shell - Support for the mongo shell is deprecated and will be removed in a future version.
v1.3.0
======
Release Summary
---------------
This release improves sharded cluster management, and adds schema validator management.
Several bug fixes improve compatibility with python3.6.
Minor Changes
-------------
- 338 - role monogdb_repository - Variablize repository details.
- 345 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Make security.keyFile configurable.
- 346 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Allow using net.bindIpAll instead of net.bindIp.
- 347 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Allow overriding net.compression.compressors in mongo*.conf
Security Fixes
--------------
- 312 - Set no_log True for ssl_keyfile.
Bugfixes
--------
- 315 - Fix exception handling for mongodb_stepdown module on python3.6
- 320 - Fix exception handling for modules mongodb_balancer, mongodb_shard, and mongodb_status.
- 352 - Add ansible.posix collection to dependencies list.
New Modules
-----------
- community.mongodb.mongodb_monitoring - Manages the free monitoring feature.
- community.mongodb.mongodb_schema - Manages MongoDB Document Schema Validators.
- community.mongodb.mongodb_shard_tag - Manage Shard Tags.
- community.mongodb.mongodb_shard_zone - Manage Shard Zones.
v1.2.1
======
Minor Changes
-------------
- 304 - Adds validate parameter to mongodb_status module.
v1.2.0
======
Release Summary
---------------
A variety of idempotency and reliability improvements.
Bugfixes
--------
- 281 - mongodb_linux Fixes disable-transparent-huge-pages.service idempotency.
- 282 - Add restart handler, and bool variable to control to mongofb_config/mongod/mongos roles.
- 285 - Output users and roles dict by database to avoid overwriting entries.
- 287 - Fixes return value on older versions of MongoDB.
- 290 - Adds pseudo-idempotency feature to module.
v1.1.2
======
Bugfixes
--------
- 252 - Fix config template override in various roles.
- 255 - Add replica_set param to mongodb_index module.
- 264 - Only add force parameter to shutdown command when set to true.
- 275 - Use OrderedDict class in the following modules, mongodb_balancer, mongodb_oplog, mongodb_shutdown.
v1.1.1
======
Bugfixes
--------
- 235 - Fix namespace.
v1.1.0
======
Release Summary
---------------
This release adds the mongodb_shell module and the mongodb_auth role.
New Modules
-----------
- community.mongodb.mongodb_shell - Run commands via the MongoDB shell.
New Roles
---------
- community.mongodb.mongodb_auth - Configure auth on MongoDB servers.
v1.0.0
======
Release Summary
---------------
The first stable release of the commmunity.mongodb collection.
Many of the plugins and modules were previously released in ansible itself.
New Plugins
-----------
Cache
~~~~~
- community.mongodb.mongodb - This cache uses per host records saved in MongoDB.
Lookup
~~~~~~
- community.mongodb.mongodb - The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.
New Modules
-----------
- community.mongodb.mongodb_balancer - Manages the MongoDB Sharded Cluster Balancer.
- community.mongodb.mongodb_index - Creates or drops indexes on MongoDB collections.
- community.mongodb.mongodb_info - Gather information about MongoDB instance.
- community.mongodb.mongodb_maintenance - Enables or disables maintenance mode for a secondary member.
- community.mongodb.mongodb_oplog - Resizes the MongoDB oplog.
- community.mongodb.mongodb_parameter - Change an administrative parameter on a MongoDB server
- community.mongodb.mongodb_replicaset - Initialises a MongoDB replicaset.
- community.mongodb.mongodb_shard - Add or remove shards from a MongoDB Cluster
- community.mongodb.mongodb_shutdown - Cleans up all database resources and then terminates the mongod/mongos process.
- community.mongodb.mongodb_status - Validates the status of the cluster.
- community.mongodb.mongodb_stepdown - Step down the MongoDB node from a PRIMARY state.
- community.mongodb.mongodb_user - Adds or removes a user from a MongoDB database
New Roles
---------
- community.mongodb.mongodb_config - Configure the CSRS Config Server Replicaset for a MongoDB sharded cluster. (Use mongodb_mongod for Standalone installations - this does not create mongo.conf)
- community.mongodb.mongodb_install - Install MongoDB packages on Debian and RedHat based platforms.
- community.mongodb.mongodb_linux - A simple role to configure Linux Operating System settings, as advised in the MongoDB Production Notes.
- community.mongodb.mongodb_mongod - Configure the mongod service (includes populating mongod.conf) which is a MongoDB replicaset or standalone server.
- community.mongodb.mongodb_mongos - Configure the mongos service (includes populating mongos.conf) which only runs in a sharded MongoDB cluster.
- community.mongodb.mongodb_repository - Configures a package repository for MongoDB on Debian and RedHat based platforms.
- community.mongodb.mongodb_selinux - Configure SELinux for MongoDB.

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"collection_info": {
"namespace": "community",
"name": "mongodb",
"version": "1.4.2",
"authors": [
"Ansible (https://github.com/ansible)",
"Rhys Campbell (https://github.com/rhysmeister)",
"Andrew Klychkov (https://github.com/Andersson007)",
"Marcos Diez (https://github.com/marcosdiez)",
"Elliott Foster (http://fourkitchens.com)",
"Loic Blot (http://www.infopro-digital.com/)",
"Matt Martz (https://github.com/sivel)",
"Jacob Floyd (https://github.com/cognifloyd)"
],
"readme": "README.md",
"tags": [
"mongodb",
"database",
"nosql"
],
"description": "MongoDB related ansible Roles, Modules, and Plugins",
"license": [],
"license_file": "COPYING",
"dependencies": {
"community.general": ">=1.0.0",
"ansible.posix": ">=1.0.0"
},
"repository": "https://github.com/ansible-collections/community.mongodb",
"documentation": "https://github.com/ansible-collections/community.mongodb/tree/master/docs",
"homepage": "https://github.com/ansible-collections/community.mongodb",
"issues": "https://github.com/ansible-collections/community.mongodb"
},
"file_manifest_file": {
"name": "FILES.json",
"ftype": "file",
"chksum_type": "sha256",
"chksum_sha256": "eb76945bef273e7e41a6840f9a55c61e10c0f1d826c5104ae02a5327d6f1380a",
"format": 1
},
"format": 1
}

View File

@@ -0,0 +1,237 @@
# MongoDB Version and PyMongoDB Version Compatibility
- This collection is tested against the most recent two minor MongoDB releases, currently 4.4.X and 5.0.X.
- This collection is tested against PyMongo version 3.12.X and latest.
- This collection will not run against any MongoDB version lower than 4.0. You can set *strict_compatibility* to false to override this behaviour but don't expect 100% success. It might be a better approach to use an older release of this collection (1.3.4 or earlier) if you're using an old MongoDB version (3.6 or earlier).
- Compatibility may be maintained for older software versions but is not guaranteed. Please upgrade your PyMongo driver version if you encounter difficulties with older versions.
- Support for PyMongo versions less than 4.X will be dropped in the future. [Drop support for pymongo < 4.0?](https://github.com/ansible-collections/community.mongodb/issues/457)
# Mongodb Collection
|Category|Status|
|---|---|
|Github CI|![CI](https://github.com/ansible-collections/community.mongodb/workflows/CI/badge.svg)|
|Github Docs|![documentation](https://github.com/ansible-collections/community.mongodb/workflows/documentation/badge.svg)|
|Codecov|[![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mongodb)](https://codecov.io/gh/ansible-collections/community.mongodb)|
|CI Roles|![CI_roles](https://github.com/ansible-collections/community.mongodb/workflows/CI_roles/badge.svg)|
|AutomatingMongoDBWithAnsible|[![CI-basic](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-basic.yml/badge.svg)](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-basic.yml)|
|AutomatingMongoDBWithAnsible|[![CI-etc](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-etc.yml/badge.svg)](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-etc.yml)|
|AutomatingMongoDBWithAnsible|[![CI-resync](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-resync.yml/badge.svg)](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-resync.yml)|
|AutomatingMongoDBWithAnsible|[![CI-upgrade-downgrade](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-upgrade-downgrade.yml/badge.svg)](https://github.com/rhysmeister/AutomatingMongoDBWithAnsible/actions/workflows/CI-upgrade-downgrade.yml)|
|Latest Build|![Build & Publish Collection](https://github.com/ansible-collections/community.mongodb/workflows/Build%20&%20Publish%20Collection/badge.svg)|
This collection called `mongodb` aims at providing all Ansible modules allowing to interact with MongoDB.
The modules present in Ansible 2.9 are included in this collection and will benefit from the evolutions and quality requirements from this collection.
As this is an independent collection, it can be released on its own release cadence.
If you like this collection please give us a rating on [Ansible Galaxy](https://galaxy.ansible.com/community/mongodb).
## Collection contents
### Roles
These roles prepare servers with Debian-based and RHEL-based distributions to run MongoDB. These roles should not be used to manage MongoDB instances that have been previously installed or configured through other means.
- `community.mongodb.mongodb_linux`: A simple role to configure Linux Operating System settings, as advised in the [MongoDB Production Notes](https://docs.mongodb.com/manual/administration/production-notes/).
- `community.mongodb.mongodb_selinux`: Configure SELinux for MongoDB.
- `community.mongodb.mongodb_repository`: Configures a package repository for MongoDB on Debian and RedHat based platforms.
- `community.mongodb.mongodb_install`: Install MongoDB packages on Debian and RedHat based platforms. This role, unlike all other roles, provides for installing specific versions of mongodb-org packages. Other roles merely validate that mongodb-org is installed/present; they do not install particular versions.
These roles manage configuring and starting various MongoDB services.
- `community.mongodb.mongodb_mongod`: Configure the `mongod` service (includes populating `mongod.conf`) which is a MongoDB replicaset or standalone server.
- `community.mongodb.mongodb_mongos`: Configure the `mongos` service (includes populating `mongos.conf`) which only runs in a sharded MongoDB cluster.
- `community.mongodb.mongodb_config`: Configure the CSRS Config Server Replicaset for a MongoDB sharded cluster. The CSRS is a special-purpose instance of `mongod` that hosts the `config` database for the sharded cluster. For standalone installations, please use the `mongodb_mongod` role instead.
- `community.mongodb.mongodb_auth`: Configure auth on MongoDB servers. NB: The other MongoDB server config roles (`mongodb_mongod`, `mongodb_mongos`, `mongodb_config`) do not configure auth. Use this role in conjunction with the other roles.
### Plugins
#### Lookup Plugins
- `community.mongodb.mongodb`: A lookup plugin that gets info from a collection using the MongoDB `find()` function.
#### Cache Plugins
- `community.mongodb.mongodb`: A cache plugin that stores the host fact cache records in MongoDB.
#### Modules
These modules are for any MongoDB cluster (standalone, replicaset, or sharded):
- `community.mongodb.mongodb_index`: Creates or drops indexes on MongoDB collections.
- `community.mongodb.mongodb_info`: Gather information about MongoDB instance.
- `community.mongodb.mongodb_monitoring`: Manages the [free monitoring](https://docs.mongodb.com/manual/administration/free-monitoring/) feature.
- `community.mongodb.mongodb_oplog`: [Resizes](https://docs.mongodb.com/manual/tutorial/change-oplog-size) the MongoDB oplog (MongoDB 3.6+ only).
- `community.mongodb.mongodb_parameter`: Change an administrative parameter on a MongoDB server.
- `community.mongodb.mongodb_schema`: Manages MongoDB Document Schema Validators.
- `community.mongodb.mongodb_shell`: Run commands via the MongoDB shell.
- `community.mongodb.mongodb_shutdown`: Cleans up all database resources and then terminates the mongod/mongos process.
- `community.mongodb.mongodb_user`: Adds or removes a user from a MongoDB database.
These modules are only useful for replicaset (or sharded) MongoDB clusters:
- `community.mongodb.mongodb_maintenance`: Enables or disables [maintenance](https://docs.mongodb.com/manual/reference/command/replSetMaintenance/) mode for a secondary member.
- `community.mongodb.mongodb_replicaset`: Initialises a MongoDB replicaset.
- `community.mongodb.mongodb_status`: Validates the status of the replicaset.
- `community.mongodb.mongodb_stepdown`: [Step down](https://docs.mongodb.com/manual/reference/command/replSetStepDown/) the MongoDB node from a PRIMARY state.
These modules are only useful for sharded MongoDB clusters:
- `community.mongodb.mongodb_balancer`: Manages the MongoDB Sharded Cluster Balancer.
- `community.mongodb.mongodb_shard`: Add or remove shards from a MongoDB Cluster.
- `community.mongodb.mongodb_shard_tag`: Manage Shard Tags.
- `community.mongodb.mongodb_shard_zone`: Manage Shard Zones.
## community.mongodb Role Tags
### General role tags
These tags are applicable across all roles.
|tags|comment|
|----|-------|
|mongodb|Tasks specific to MongoDB.|
|debian|Tasks specific to Debian Family Operating Systems.|
|redhat|Tasks specific to RedHat Family Operating Systems.|
|pip|Tasks working with pip.|
|vars|Tasks that load variables.|
|pkg|Tasks that install packages.|
|debug|Tasks that output debugging info.|
|service|Tasks dealing with system services.|
|setup|Tasks that are mainly executed during initial deployment.|
|ci|Tasks that are specific to the community.mongodb CI code.|
|linux|Tasks affecting Linux OS settings.|
### Role Specific Tags
These tags apply to the specific roles as indicated.
|role|tag|comment|
|----|---|-------|
|mongodb_auth|admin_user|Tasks that work with the MongoDB Administrator user.|
|mongodb_auth|app_user|Tasks that work with MongoDB app users.|
## Running the integration and unit tests
* Requirements
* [Python 3.5+](https://www.python.org/)
* [pip](https://pypi.org/project/pip/)
* [virtualenv](https://virtualenv.pypa.io/en/latest/) or [pipenv](https://pypi.org/project/pipenv/) if you prefer.
* [git](https://git-scm.com/)
* [docker](https://www.docker.com/)
* Useful Links
* [Pip & Virtual Environments](https://docs.python-guide.org/dev/virtualenvs/)
* [Ansible Integration Tests](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html)
The ansible-test tool requires a specific directory hierarchy to function correctly so please follow carefully.
* Create the required directory structure. N-B. The ansible-test tool requires this format.
```bash
mkdir -p git/ansible_collections/community
cd git/ansible_collections/community
```
* Clone the required projects.
```bash
git clone https://github.com/ansible-collections/community.mongodb.git ./mongodb
git clone https://github.com/ansible-collections/community.general.git ./general
```
* Create and activate a virtual environment.
```bash
virtualenv venv
source venv/bin/activate
```
* Change to the project directory.
```bash
cd mongodb
```
* Install the devel branch of ansible-base.
```bash
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
```
* Run integration tests for the mongodb_shard module.
```bash
ansible-test integration --docker default -v --color --python 3.6 mongodb_shard
```
* Run integration tests for the mongodb_status module.
```bash
ansible-test integration --docker default -v --color --python 3.6 mongodb_status
```
* Run integration tests for the mongodb_oplog module.
```bash
ansible-test integration --docker ubuntu1804 -v --color --python 3.6 mongodb_oplog
```
* Run tests for everything in the collection.
```bash
ansible-test integration --docker default -v --color --python 3.6
```
* Run the units tests
```bash
ansible-test units --docker default -v --color --python 3.6
```
## Release Notes
Needs improvement but the general process for issuing a new release to Ansible Galaxy is as follows...
- View commits since last release and copy text for release notes
```bash
git log 1.3.0..HEAD
git log 1.3.0..HEAD --oneline
```
- Create a new branch
- Update galaxy.yml with version and any other appropriate info
- Update changelogs/changelog.yaml
- Update CHANGELOG.rst
- Create a pull request
- Review and merge when happy
- Tag release on the master branch
```bash
git tag <release>
git push --tags
```
Automation will bundle the release and push to Galaxy. Should take around 10-15 minutes.
## GitHub workflow
* Maintainers would be members of this GitHub Repo
* Branch protections could be used to enforce 1 (or 2) reviews from relevant maintainers [CODEOWNERS](.github/CODEOWNERS)
## Contributing
Any contribution is welcome and we only ask contributors to:
* Provide *at least* integration tests for any contribution.
* Create an issue for any significant contribution that would change a large portion of the codebase.
## Stargazers over time
[![Stargazers over time](https://starchart.cc/ansible-collections/community.mongodb.svg)](https://starchart.cc/ansible-collections/community.mongodb)
## License
GNU General Public License v3.0 or later
See LICENCING to see the full text.

View File

@@ -0,0 +1,110 @@
objects: {}
plugins:
become: {}
cache:
mongodb:
description: Use MongoDB for caching
name: mongodb
version_added: null
callback: {}
cliconf: {}
connection: {}
httpapi: {}
inventory: {}
lookup:
mongodb:
description: lookup info from MongoDB
name: mongodb
version_added: '2.3'
module:
mongodb_balancer:
description: Manages the MongoDB Sharded Cluster Balancer.
name: mongodb_balancer
namespace: ''
version_added: 1.0.0
mongodb_index:
description: Creates or drops indexes on MongoDB collections.
name: mongodb_index
namespace: ''
version_added: 1.0.0
mongodb_info:
description: Gather information about MongoDB instance.
name: mongodb_info
namespace: ''
version_added: 1.0.0
mongodb_maintenance:
description: Enables or disables maintenance mode for a secondary member.
name: mongodb_maintenance
namespace: ''
version_added: 1.0.0
mongodb_monitoring:
description: Manages the free monitoring feature.
name: mongodb_monitoring
namespace: ''
version_added: 1.3.0
mongodb_oplog:
description: Resizes the MongoDB oplog.
name: mongodb_oplog
namespace: ''
version_added: 1.0.0
mongodb_parameter:
description: Change an administrative parameter on a MongoDB server
name: mongodb_parameter
namespace: ''
version_added: 1.0.0
mongodb_replicaset:
description: Initialises a MongoDB replicaset.
name: mongodb_replicaset
namespace: ''
version_added: 1.0.0
mongodb_schema:
description: Manages MongoDB Document Schema Validators.
name: mongodb_schema
namespace: ''
version_added: 1.3.0
mongodb_shard:
description: Add or remove shards from a MongoDB Cluster
name: mongodb_shard
namespace: ''
version_added: 1.0.0
mongodb_shard_tag:
description: Manage Shard Tags.
name: mongodb_shard_tag
namespace: ''
version_added: 1.3.0
mongodb_shard_zone:
description: Manage Shard Zones.
name: mongodb_shard_zone
namespace: ''
version_added: 1.3.0
mongodb_shell:
description: Run commands via the MongoDB shell.
name: mongodb_shell
namespace: ''
version_added: 1.1.0
mongodb_shutdown:
description: Cleans up all database resources and then terminates the mongod/mongos
process.
name: mongodb_shutdown
namespace: ''
version_added: 1.0.0
mongodb_status:
description: Validates the status of the replicaset.
name: mongodb_status
namespace: ''
version_added: 1.0.0
mongodb_stepdown:
description: Step down the MongoDB node from a PRIMARY state.
name: mongodb_stepdown
namespace: ''
version_added: 1.0.0
mongodb_user:
description: Adds or removes a user from a MongoDB database
name: mongodb_user
namespace: ''
version_added: 1.0.0
netconf: {}
shell: {}
strategy: {}
vars: {}
version: 1.3.0

View File

@@ -0,0 +1,229 @@
ancestor: null
releases:
1.0.0:
changes:
release_summary: |
The first stable release of the commmunity.mongodb collection.
Many of the plugins and modules were previously released in ansible itself.
modules:
- description: Manages the MongoDB Sharded Cluster Balancer.
name: mongodb_balancer
namespace: ''
- description: Creates or drops indexes on MongoDB collections.
name: mongodb_index
namespace: ''
- description: Gather information about MongoDB instance.
name: mongodb_info
namespace: ''
- description: Enables or disables maintenance mode for a secondary member.
name: mongodb_maintenance
namespace: ''
- description: Resizes the MongoDB oplog.
name: mongodb_oplog
namespace: ''
- description: Change an administrative parameter on a MongoDB server
name: mongodb_parameter
namespace: ''
- description: Initialises a MongoDB replicaset.
name: mongodb_replicaset
namespace: ''
- description: Add or remove shards from a MongoDB Cluster
name: mongodb_shard
namespace: ''
- description: Cleans up all database resources and then terminates the mongod/mongos
process.
name: mongodb_shutdown
namespace: ''
- description: Validates the status of the cluster.
name: mongodb_status
namespace: ''
- description: Step down the MongoDB node from a PRIMARY state.
name: mongodb_stepdown
namespace: ''
- description: Adds or removes a user from a MongoDB database
name: mongodb_user
namespace: ''
plugins:
cache:
- description: This cache uses per host records saved in MongoDB.
name: mongodb
namespace: ''
lookup:
- description: 'The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.'
name: mongodb
namespace: ''
objects:
role:
- description: Configure the CSRS Config Server Replicaset for a MongoDB sharded
cluster. (Use mongodb_mongod for Standalone installations - this does not
create mongo.conf)
name: mongodb_config
namespace: null
- description: Install MongoDB packages on Debian and RedHat based platforms.
name: mongodb_install
namespace: null
- description: A simple role to configure Linux Operating System settings, as
advised in the MongoDB Production Notes.
name: mongodb_linux
namespace: null
- description: Configure the mongod service (includes populating mongod.conf)
which is a MongoDB replicaset or standalone server.
name: mongodb_mongod
namespace: null
- description: Configure the mongos service (includes populating mongos.conf)
which only runs in a sharded MongoDB cluster.
name: mongodb_mongos
namespace: null
- description: Configures a package repository for MongoDB on Debian and RedHat
based platforms.
name: mongodb_repository
namespace: null
- description: Configure SELinux for MongoDB.
name: mongodb_selinux
namespace: null
release_date: '2020-08-06'
1.1.0:
changes:
release_summary: |
This release adds the mongodb_shell module and the mongodb_auth role.
modules:
- description: Run commands via the MongoDB shell.
name: mongodb_shell
namespace: ''
objects:
role:
- description: Configure auth on MongoDB servers.
name: mongodb_auth
namespace: null
release_date: '2020-10-24'
1.1.1:
changes:
bugfixes:
- 235 - Fix namespace.
release_date: '2020-11-04'
1.1.2:
changes:
bugfixes:
- 252 - Fix config template override in various roles.
- 255 - Add replica_set param to mongodb_index module.
- 264 - Only add force parameter to shutdown command when set to true.
- 275 - Use OrderedDict class in the following modules, mongodb_balancer, mongodb_oplog,
mongodb_shutdown.
release_date: '2021-01-06'
1.2.0:
changes:
release_summary: |
A variety of idempotency and reliability improvements.
bugfixes:
- 281 - mongodb_linux Fixes disable-transparent-huge-pages.service idempotency.
- 282 - Add restart handler, and bool variable to control to mongofb_config/mongod/mongos
roles.
- 285 - Output users and roles dict by database to avoid overwriting entries.
- 287 - Fixes return value on older versions of MongoDB.
- 290 - Adds pseudo-idempotency feature to module.
release_date: '2021-01-31'
1.2.1:
changes:
minor_changes:
- 304 - Adds validate parameter to mongodb_status module.
release_date: '2021-02-09'
1.3.0:
changes:
release_summary: |
This release improves sharded cluster management, and adds schema validator management.
Several bug fixes improve compatibility with python3.6.
security_fixes:
- 312 - Set no_log True for ssl_keyfile.
bugfixes:
- 315 - Fix exception handling for mongodb_stepdown module on python3.6
- 320 - Fix exception handling for modules mongodb_balancer, mongodb_shard,
and mongodb_status.
- 352 - Add ansible.posix collection to dependencies list.
minor_changes:
- 338 - role monogdb_repository - Variablize repository details.
- 345 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Make security.keyFile
configurable.
- 346 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Allow using net.bindIpAll
instead of net.bindIp.
- 347 - roles mongodb_config, mongodb_mongod, mongodb_mongos - Allow overriding
net.compression.compressors in mongo*.conf
modules:
- description: Manages the free monitoring feature.
name: mongodb_monitoring
namespace: ''
- description: Manages MongoDB Document Schema Validators.
name: mongodb_schema
namespace: ''
- description: Manage Shard Tags.
name: mongodb_shard_tag
namespace: ''
- description: Manage Shard Zones.
name: mongodb_shard_zone
namespace: ''
release_date: '2021-07-20'
1.3.1:
release_summary: |
This release is a maintenance release. The GitHub CI has been updated to include MongoDB 5.0 as well
as a few new features. The mongosh shell is now supported in the mongodb_shell module. Support for the
old mongo shell will be removed in a future release.
minor_changes:
- 360 - mongodb_shell - Adds support for the mongosh shell now available with MongoDB 5.0.
- 368 - mongodb_shell - Use shlex escape function.
- 370 - mongodb_install - Adds mongodb_hold_packages variable. Runs the lock_mongodb_packages.sh script
to either lock mongodb-org packages at a specific version or to release the lock.
Set to "HOLD" or "NOHOLD" as desired. No checks are made to see if the hold already exists or not.
By default this variable is undefined and the script is not executed.
The task is executed at the end and it is possible that packages could be upgraded
before the lock is initially applied.
deprecated_features:
- mongodb_shell - Support for the mongo shell is deprecated and will be removed in a future version.
1.3.2:
release_summary: |
This release is a maintenance release.
minor_changes:
- 413 - mongodb_shell - Adds escape_param function that will work better across various python versions.
- 414-416 - Minor documentation improvements.
- 411 - mongodb_shell - FIx missing db parameter when fiel parmeter is used.
- 403 - Make db path configurable in roles.
- 401 - mongodb_replicaset - Add further examples.
- 399 - Removes unused imports from modules.
- 396 - Add tags to roles.
- 387 - Fix documentation for mongod cache plugin.
major_changes:
- 397 & 376 - mongodb_replicaset - Add reconfigure abilities to module. Add and removes members from replicasets.
1.3.3:
release_summary: |
This release is a maintenance release.
bugfixes:
- 448 - Fix issue in roles where mongod does not restart when a custom bind_ip is set.
- 440 - Fix incorrect alias ssl_crlfile.
- 450 - Fix issues with mongodb_replicaset connecting with the pymongo 4.0.X driver.
minor_changes:
- 450 - mongodb_replicaset. Introduce cluster_cmd parameter. Can be set to isMaster or hello.
hello is the default. isMaster is useful for older versions of MongoDB.
See [db.hello()](https://www.mongodb.com/docs/manual/reference/method/db.hello/) for more.
1.3.4:
release_summary: |
This release is a maintenance release.
bugfixes:
- 466 & 467 - Fixes localhost exception bug due to directConnection parameter in newer pymongo versions.
1.4.0:
release_summary: |
This release is a maintenance release. Pymongo versions 3.12.* or 4.* are now required. MongoDB version 4+ are also required but can be overriden if desired.
major_changes:
- 470 - Removes depreciated distutils package and require Pymongo 3.12+ and MongoDB 4+
Adds a new parameter strict_compatibility (default true).
Set to false to disable Pymongo and MongoDB requirements.
1.4.1:
release_summary: |
This release is a maintenance release.
minor_changes:
- 474 - Adds log_path parameter to mongodb_mongod, mongodb_mongos and mongodb_config roles.
bugfixes:
- 479 - mongodb_shell - Correct supports_check_mode value. Used to be true, which is wrong, now false.
1.4.2:
release_summary: |
This release is a maintenance release.
minor_changes:
- 483 - Removes previous upper bound restriction for communiry.general collection,
- 483 - Use extended FQCN for pam_limits (community.general.system.pam_limits instead of community.general.pam_limits).

View File

@@ -0,0 +1,31 @@
changelog_filename_template: ../CHANGELOG.rst
changelog_filename_version_depth: 0
changes_file: changelog.yaml
changes_format: combined
ignore_other_fragment_extensions: true
keep_fragments: false
mention_ancestor: true
new_plugins_after_name: removed_features
notesdir: fragments
prelude_section_name: release_summary
prelude_section_title: Release Summary
sections:
- - major_changes
- Major Changes
- - minor_changes
- Minor Changes
- - breaking_changes
- Breaking Changes / Porting Guide
- - deprecated_features
- Deprecated Features
- - removed_features
- Removed Features (previously deprecated)
- - security_fixes
- Security Fixes
- - bugfixes
- Bugfixes
- - known_issues
- Known Issues
title: Community.MongoDB
trivial_section_name: trivial
use_fqcn: true

View File

@@ -0,0 +1,2 @@
---
requires_ansible: '>=2.9.10'

View File

@@ -0,0 +1,204 @@
# (c) 2018, Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
name: mongodb
author:
- Matt Martz (@sivel)
version_added: "1.0.0"
short_description: Use MongoDB for caching
description:
- This cache uses per host records saved in MongoDB.
requirements:
- pymongo>=3
options:
_uri:
description:
- MongoDB Connection String URI
required: False
env:
- name: ANSIBLE_CACHE_PLUGIN_CONNECTION
ini:
- key: fact_caching_connection
section: defaults
_prefix:
description: User defined prefix to use when creating the DB entries
default: ansible_facts
env:
- name: ANSIBLE_CACHE_PLUGIN_PREFIX
ini:
- key: fact_caching_prefix
section: defaults
_timeout:
default: 86400
description: Expiration timeout in seconds for the cache plugin data. Set to 0 to never expire
env:
- name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
ini:
- key: fact_caching_timeout
section: defaults
type: integer
'''
import datetime
from contextlib import contextmanager
from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.plugins.cache import BaseCacheModule
from ansible.utils.display import Display
from ansible.module_utils._text import to_native
pymongo_missing = False
try:
import pymongo
except ImportError:
pymongo_missing = True
display = Display()
class CacheModule(BaseCacheModule):
"""
A caching module backed by mongodb.
"""
def __init__(self, *args, **kwargs):
try:
if pymongo_missing:
raise AnsibleError("The 'pymongo' python module is required for the mongodb fact cache, 'pip install pymongo>=3.0'")
super(CacheModule, self).__init__(*args, **kwargs)
self._connection = self.get_option('_uri')
self._timeout = int(self.get_option('_timeout'))
self._prefix = self.get_option('_prefix')
except KeyError:
self._connection = C.CACHE_PLUGIN_CONNECTION
self._timeout = int(C.CACHE_PLUGIN_TIMEOUT)
self._prefix = C.CACHE_PLUGIN_PREFIX
self._cache = {}
self._managed_indexes = False
def _ttl_index_exists(self, collection):
'''
Returns true if an index named ttl exists
on the given collection.
'''
exists = False
try:
indexes = collection.list_indexes()
for index in indexes:
if index["name"] == "ttl":
exists = True
break
except pymongo.errors.OperationFailure as excep:
raise AnsibleError('Error checking MongoDB index: %s' % to_native(excep))
return exists
def _manage_indexes(self, collection):
'''
This function manages indexes on the mongo collection.
We only do this once, at run time based on _managed_indexes,
rather than per connection instantiation as that would be overkill
'''
_timeout = self._timeout
if _timeout and _timeout > 0:
try:
collection.create_index(
'date',
name='ttl',
expireAfterSeconds=_timeout
)
except pymongo.errors.OperationFailure:
# We make it here when the fact_caching_timeout was set to a different value between runs
if self._ttl_index_exists(collection):
collection.drop_index('ttl')
return self._manage_indexes(collection)
else:
if self._ttl_index_exists(collection):
collection.drop_index('ttl')
@contextmanager
def _collection(self):
'''
This is a context manager for opening and closing mongo connections as needed. This exists as to not create a global
connection, due to pymongo not being fork safe (http://api.mongodb.com/python/current/faq.html#is-pymongo-fork-safe)
'''
mongo = pymongo.MongoClient(self._connection)
try:
db = mongo.get_default_database()
except pymongo.errors.ConfigurationError:
# We'll fall back to using ``ansible`` as the database if one was not provided
# in the MongoDB Connection String URI
db = mongo['ansible']
# The collection is hard coded as ``cache``, there are no configuration options for this
collection = db['cache']
if not self._managed_indexes:
# Only manage the indexes once per run, not per connection
self._manage_indexes(collection)
self._managed_indexes = True
yield collection
mongo.close()
def _make_key(self, key):
return '%s%s' % (self._prefix, key)
def get(self, key):
if key not in self._cache:
with self._collection() as collection:
value = collection.find_one({'_id': self._make_key(key)})
self._cache[key] = value['data']
return self._cache.get(key)
def set(self, key, value):
self._cache[key] = value
with self._collection() as collection:
collection.update_one(
{'_id': self._make_key(key)},
{
'$set': {
'_id': self._make_key(key),
'data': value,
'date': datetime.datetime.utcnow()
}
},
upsert=True
)
def keys(self):
with self._collection() as collection:
return [doc['_id'] for doc in collection.find({}, {'_id': True})]
def contains(self, key):
with self._collection() as collection:
return bool(collection.count({'_id': self._make_key(key)}))
def delete(self, key):
del self._cache[key]
with self._collection() as collection:
collection.delete_one({'_id': self._make_key(key)})
def flush(self):
with self._collection() as collection:
collection.delete_many({})
def copy(self):
with self._collection() as collection:
return dict((d['_id'], d['data']) for d in collection.find({}))
def __getstate__(self):
return dict()
def __setstate__(self, data):
self.__init__()

View File

@@ -0,0 +1,44 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Standard documentation
DOCUMENTATION = r'''
options:
login_user:
description:
- The MongoDB user to login with.
- Required when I(login_password) is specified.
required: no
type: str
login_password:
description:
- The password used to authenticate with.
- Required when I(login_user) is specified.
required: no
type: str
login_database:
description:
- The database where login credentials are stored.
required: no
type: str
default: 'admin'
login_host:
description:
- The host running MongoDB instance to login to.
required: no
type: str
default: 'localhost'
login_port:
description:
- The MongoDB server port to login to.
required: no
type: int
default: 27017
strict_compatibility:
description:
- Enforce strict requirements for pymongo and MongoDB software versions
type: bool
default: True
'''

View File

@@ -0,0 +1,79 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Standard documentation
DOCUMENTATION = r'''
options:
ssl:
description:
- Whether to use an SSL connection when connecting to the database.
required: no
type: bool
default: no
aliases:
- tls
ssl_cert_reqs:
description:
- Specifies whether a certificate is required from the other side of the connection,
and whether it will be validated if provided.
required: no
type: str
default: 'CERT_REQUIRED'
choices:
- 'CERT_NONE'
- 'CERT_OPTIONAL'
- 'CERT_REQUIRED'
aliases:
- tlsAllowInvalidCertificates
ssl_ca_certs:
description:
- The ssl_ca_certs option takes a path to a CA file.
required: no
type: str
aliases:
- tlsCAFile
ssl_crlfile:
description:
- The ssl_crlfile option takes a path to a CRL file.
required: no
type: str
ssl_certfile:
description:
- Present a client certificate using the ssl_certfile option.
required: no
type: str
aliases:
- tlsCertificateKeyFile
ssl_keyfile:
description:
- Private key for the client certificate.
required: no
type: str
ssl_pem_passphrase:
description:
- Passphrase to decrypt encrypted private keys.
required: no
type: str
aliases:
- tlsCertificateKeyFilePassword
auth_mechanism:
description:
- Authentication type.
required: no
type: str
choices:
- 'SCRAM-SHA-256'
- 'SCRAM-SHA-1'
- 'MONGODB-X509'
- 'GSSAPI'
- 'PLAIN'
connection_options:
description:
- Additional connection options.
- Supply as a list of dicts or strings containing key value pairs seperated with '='.
required: no
type: list
elements: raw
'''

View File

@@ -0,0 +1,269 @@
# (c) 2016, Marcos Diez <marcos@unitron.com.br>
# https://github.com/marcosdiez/
#
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: mongodb
author:
- Marcos Diez (@marcosdiez)
version_added: "1.0.0"
short_description: lookup info from MongoDB
description:
- 'The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.'
- 'The result is a list of jsons, so slightly different from what PyMongo returns. In particular, *timestamps* are converted to epoch integers.'
options:
connect_string:
description:
- Can be any valid MongoDB connection string, supporting authentication, replica sets, etc.
- "More info at U(https://docs.mongodb.org/manual/reference/connection-string/)"
default: "mongodb://localhost/"
database:
description:
- Name of the database which the query will be made
required: True
collection:
description:
- Name of the collection which the query will be made
required: True
filter:
description:
- Criteria of the output
type: 'dict'
default: {}
projection:
description:
- Fields you want returned
type: dict
default: {}
skip:
description:
- How many results should be skipped
type: integer
limit:
description:
- How many results should be shown
type: integer
sort:
description:
- Sorting rules.
- Please use the strings C(ASCENDING) and C(DESCENDING) to set the order.
- Check the example for more information.
type: list
elements: list
default: []
extra_connection_parameters:
description:
- Extra connection parameters that to be sent to pymongo.MongoClient
- Check the example to see how to connect to mongo using an SSL certificate.
- "All possible parameters are here: U(https://api.mongodb.com/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient)"
type: dict
default: {}
notes:
- "Please check https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find for more details."
requirements:
- pymongo >= 2.4 (python library)
'''
EXAMPLES = '''
- hosts: localhost
gather_facts: false
vars:
mongodb_parameters:
#mandatory parameters
database: 'local'
collection: "startup_log"
#optional
connection_string: "mongodb://localhost/"
# connection_string: "mongodb://username:password@my.server.com:27017/"
# extra_connection_parameters: { "ssl" : True , "ssl_certfile": /etc/self_signed_certificate.pem" }
#optional query parameters, we accept any parameter from the normal mongodb query.
# filter: { "hostname": "u18" }
projection: { "pid": True , "_id" : False , "hostname" : True }
skip: 0
limit: 1
sort: [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ]
tasks:
- debug: msg="The PID from MongoDB is {{ lookup('mongodb', mongodb_parameters ).pid }}"
- debug: msg="The HostName from the MongoDB server is {{ lookup('mongodb', mongodb_parameters ).hostname }}"
- debug: msg="Mongo DB is stored at {{ lookup('mongodb', mongodb_parameters_inline )}}"
vars:
mongodb_parameters_inline:
database: 'local'
collection: "startup_log"
connection_string: "mongodb://localhost/"
limit: 1
projection: { "cmdline.storage": True }
# lookup syntax, does the same as below
- debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
loop: "{{ lookup('mongodb', mongodb_parameters, wantlist=True) }}"
# query syntax, does the same as above
- debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
loop: "{{ query('mongodb', mongodb_parameters) }}"
- name: "Raw output from the mongodb lookup (a json with pid and hostname )"
debug: msg="{{ lookup('mongodb', mongodb_parameters) }}"
- name: "Yet another mongodb query, now with the parameters on the task itself"
debug: msg="pid={{item.pid}} hostname={{item.hostname}} version={{ item.buildinfo.version }}"
with_mongodb:
- database: 'local'
collection: "startup_log"
connection_string: "mongodb://localhost/"
limit: 1
projection: { "pid": True , "hostname": True , "buildinfo.version": True }
# Please notice this specific query may result more than one result. This is expected
- name: "Shows the whole output from mongodb"
debug: msg="{{ item }}"
with_mongodb:
- database: 'local'
collection: "startup_log"
connection_string: "mongodb://localhost/"
'''
RETURN = """
_list_of_jsons:
description:
- a list of JSONs with the results of the MongoDB query.
type: list
"""
import datetime
from ansible.module_utils.six import string_types, integer_types
from ansible.module_utils._text import to_native
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
try:
from pymongo import ASCENDING, DESCENDING
from pymongo.errors import ConnectionFailure
from pymongo import MongoClient
except ImportError:
try: # for older PyMongo 2.2
from pymongo import Connection as MongoClient
except ImportError:
pymongo_found = False
else:
pymongo_found = True
else:
pymongo_found = True
class LookupModule(LookupBase):
def _fix_sort_parameter(self, sort_parameter):
if sort_parameter is None:
return sort_parameter
if not isinstance(sort_parameter, list):
raise AnsibleError(u"Error. Sort parameters must be a list, not [ {0} ]".format(sort_parameter))
for item in sort_parameter:
self._convert_sort_string_to_constant(item)
return sort_parameter
def _convert_sort_string_to_constant(self, item):
original_sort_order = item[1]
sort_order = original_sort_order.upper()
if sort_order == u"ASCENDING":
item[1] = ASCENDING
elif sort_order == u"DESCENDING":
item[1] = DESCENDING
# else the user knows what s/he is doing and we won't predict. PyMongo will return an error if necessary
def convert_mongo_result_to_valid_json(self, result):
if result is None:
return result
if isinstance(result, integer_types + (float, bool)):
return result
if isinstance(result, string_types):
return result
elif isinstance(result, list):
new_list = []
for elem in result:
new_list.append(self.convert_mongo_result_to_valid_json(elem))
return new_list
elif isinstance(result, dict):
new_dict = {}
for key in result.keys():
value = result[key] # python2 and 3 compatible....
new_dict[key] = self.convert_mongo_result_to_valid_json(value)
return new_dict
elif isinstance(result, datetime.datetime):
# epoch
return (result - datetime.datetime(1970, 1, 1)). total_seconds()
else:
# failsafe
return u"{0}".format(result)
def run(self, terms, variables, **kwargs):
try:
return self._run_helper(terms)
except Exception as e:
print(u"There was an exception on the mongodb_lookup: {0}".format(to_native(e)))
raise e
def _run_helper(self, terms):
if not pymongo_found:
raise AnsibleError(u"pymongo is required in the control node (this machine) for mongodb lookup.")
ret = []
for term in terms:
for required_parameter in [u"database", u"collection"]:
if required_parameter not in term:
raise AnsibleError(u"missing mandatory parameter [{0}]".format(required_parameter))
connection_string = term.get(u'connection_string', u"mongodb://localhost")
database = term[u"database"]
collection = term[u'collection']
extra_connection_parameters = term.get(u'extra_connection_parameters', {})
if u"extra_connection_parameters" in term:
del term[u"extra_connection_parameters"]
if u"connection_string" in term:
del term[u"connection_string"]
del term[u"database"]
del term[u"collection"]
if u"sort" in term:
term[u"sort"] = self._fix_sort_parameter(term[u"sort"])
# all other parameters are sent to mongo, so we are future and past proof
try:
client = MongoClient(connection_string, **extra_connection_parameters)
results = client[database][collection].find(**term)
for result in results:
result = self.convert_mongo_result_to_valid_json(result)
ret.append(result)
except ConnectionFailure as e:
raise AnsibleError(u'unable to connect to database: %s' % str(e))
return ret

View File

@@ -0,0 +1,410 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six.moves import configparser
from ansible.module_utils._text import to_native
import traceback
import os
import ssl as ssl_lib
MongoClient = None
PYMONGO_IMP_ERR = None
pymongo_found = None
PyMongoVersion = None
ConnectionFailure = None
OperationFailure = None
try:
from pymongo.errors import ConnectionFailure
from pymongo.errors import OperationFailure
from pymongo import version as PyMongoVersion
from pymongo import MongoClient
pymongo_found = True
except ImportError:
PYMONGO_IMP_ERR = traceback.format_exc()
pymongo_found = False
def check_compatibility(module, srv_version, driver_version):
if driver_version.startswith('3.12') or driver_version.startswith('4'):
if int(srv_version[0]) < 4:
if module.params['strict_compatibility']:
module.fail_json("This version of MongoDB is pretty old and these modules are no longer tested against this version.")
else:
module.warn("This version of MongoDB is pretty old and these modules are no longer tested against this version.")
else:
if module.params['strict_compatibility']:
module.fail_json("You must use pymongo 3.12+ or 4+.")
else:
module.warn("You should use pymongo 3.12+ or 4+ but {0} was found.".format(driver_version))
def load_mongocnf():
config = configparser.RawConfigParser()
mongocnf = os.path.expanduser('~/.mongodb.cnf')
try:
config.readfp(open(mongocnf))
except (configparser.NoOptionError, IOError):
return False
creds = dict(
user=config.get('client', 'user'),
password=config.get('client', 'pass')
)
return creds
def index_exists(client, database, collection, index_name):
"""
Returns true if an index on the collection exists with the given name
@client: MongoDB connection.
@database: MongoDB Database.
@collection: MongoDB collection.
@index_name: The index name.
"""
exists = False
indexes = client[database][collection].list_indexes()
for index in indexes:
if index["name"] == index_name:
exists = True
return exists
def create_index(client, database, collection, keys, options):
"""
Creates an index on the given collection
@client: MongoDB connection.
@database: MongoDB Database - str.
@collection: MongoDB collection - str.
@keys: Specification of index - dict.
"""
client[database][collection].create_index(list(keys.items()),
**options)
def drop_index(client, database, collection, index_name):
client[database][collection].drop_index(index_name)
def member_state(client):
"""Check if a replicaset exists.
Args:
client (cursor): Mongodb cursor on admin database.
Returns:
str: member state i.e. PRIMARY, SECONDARY
"""
state = None
doc = client['admin'].command('replSetGetStatus')
for member in doc["members"]:
if "self" in member.keys():
state = str(member['stateStr'])
return state
def mongodb_common_argument_spec(ssl_options=True):
"""
Returns a dict containing common options shared across the MongoDB modules.
"""
options = dict(
login_user=dict(type='str', required=False),
login_password=dict(type='str', required=False, no_log=True),
login_database=dict(type='str', required=False, default='admin'),
login_host=dict(type='str', required=False, default='localhost'),
login_port=dict(type='int', required=False, default=27017),
strict_compatibility=dict(type='bool', default=True),
)
ssl_options_dict = dict(
ssl=dict(type='bool', required=False, default=False, aliases=['tls']),
ssl_cert_reqs=dict(type='str',
required=False,
default='CERT_REQUIRED',
choices=['CERT_NONE',
'CERT_OPTIONAL',
'CERT_REQUIRED'],
aliases=['tlsAllowInvalidCertificates']),
ssl_ca_certs=dict(type='str', default=None, aliases=['tlsCAFile']),
ssl_crlfile=dict(type='str', default=None),
ssl_certfile=dict(type='str', default=None, aliases=['tlsCertificateKeyFile']),
ssl_keyfile=dict(type='str', default=None, no_log=True),
ssl_pem_passphrase=dict(type='str', default=None, no_log=True, aliases=['tlsCertificateKeyFilePassword']),
auth_mechanism=dict(type='str',
required=False,
default=None,
choices=['SCRAM-SHA-256',
'SCRAM-SHA-1',
'MONGODB-X509',
'GSSAPI',
'PLAIN']),
connection_options=dict(type='list',
elements='raw',
default=None)
)
if ssl_options:
options.update(ssl_options_dict)
return options
def rename_ssl_option_for_pymongo4(connection_options):
"""
This function renames the old ssl parameter, and sorts the data out,
when the driver use is >= PyMongo 4
"""
if int(PyMongoVersion[0]) >= 4:
if connection_options.get('ssl_cert_reqs', None) == 'CERT_NONE':
connection_options['tlsAllowInvalidCertificates'] = False
elif connection_options.get('ssl_cert_reqs', None) == 'CERT_REQUIRED':
connection_options['tlsAllowInvalidCertificates'] = False
connection_options.pop('ssl_cert_reqs', None)
if connection_options.get('ssl_ca_certs', None) is not None:
connection_options['tlsCAFile'] = connection_options['ssl_ca_certs']
connection_options.pop('ssl_ca_certs', None)
connection_options.pop('ssl_crlfile', None)
if connection_options.get('ssl_certfile', None) is not None:
connection_options['tlsCertificateKeyFile'] = connection_options['ssl_certfile']
elif connection_options.get('ssl_keyfile', None) is not None:
connection_options['tlsCertificateKeyFile'] = connection_options['ssl_keyfile']
connection_options.pop('ssl_certfile', None)
connection_options.pop('ssl_keyfile', None)
if connection_options.get('ssl_pem_passphrase', None) is not None:
connection_options['tlsCertificateKeyFilePassword'] = connection_options['ssl_pem_passphrase']
connection_options.pop('ssl_pem_passphrase', None)
return connection_options
def add_option_if_not_none(param_name, module, connection_params):
'''
@param_name - The parameter name to check
@module - The ansible module object
@connection_params - Dict containing the connection params
'''
if module.params[param_name] is not None:
connection_params[param_name] = module.params[param_name]
return connection_params
def ssl_connection_options(connection_params, module):
connection_params['ssl'] = True
if module.params['ssl_cert_reqs'] is not None:
connection_params['ssl_cert_reqs'] = getattr(ssl_lib, module.params['ssl_cert_reqs'])
connection_params = add_option_if_not_none('ssl_ca_certs', module, connection_params)
connection_params = add_option_if_not_none('ssl_crlfile', module, connection_params)
connection_params = add_option_if_not_none('ssl_certfile', module, connection_params)
connection_params = add_option_if_not_none('ssl_keyfile', module, connection_params)
connection_params = add_option_if_not_none('ssl_pem_passphrase', module, connection_params)
if module.params['auth_mechanism'] is not None:
connection_params['authMechanism'] = module.params['auth_mechanism']
if module.params['connection_options'] is not None:
for item in module.params['connection_options']:
if isinstance(item, dict):
for key, value in item.items():
connection_params[key] = value
elif isinstance(item, str) and "=" in item:
connection_params[item.split('=')[0]] = item.split('=')[1]
else:
raise ValueError("Invalid value supplied in connection_options: {0} .".format(str(item)))
return connection_params
def check_srv_version(module, client):
try:
srv_version = client.server_info()['version']
except Exception as excep:
module.fail_json(msg='Unable to get MongoDB server version: %s' % to_native(excep))
return srv_version
def check_driver_compatibility(module, client, srv_version):
try:
# Get driver version::
driver_version = PyMongoVersion
# Check driver and server version compatibility:
check_compatibility(module, srv_version, driver_version)
except Exception as excep:
module.fail_json(msg='Unable to check driver compatibility: %s' % to_native(excep))
def get_mongodb_client(module, login_user=None, login_password=None, login_database=None, directConnection=False):
"""
Build the connection params dict and returns a MongoDB Client object
"""
connection_params = {
'host': module.params['login_host'],
'port': module.params['login_port'],
}
if directConnection:
connection_params['directConnection'] = True
if module.params['ssl']:
connection_params = ssl_connection_options(connection_params, module)
connection_params = rename_ssl_option_for_pymongo4(connection_params)
# param exists only in some modules
if 'replica_set' in module.params and 'reconfigure' not in module.params:
connection_params["replicaset"] = module.params['replica_set']
elif 'replica_set' in module.params and 'reconfigure' in module.params \
and module.params['reconfigure']:
connection_params["replicaset"] = module.params['replica_set']
if login_user:
connection_params['username'] = login_user
connection_params['password'] = login_password
connection_params['authSource'] = login_database
client = MongoClient(**connection_params)
return client
def is_auth_enabled(module):
"""
Returns True if auth is enabled on the mongo instance
For PyMongo 4+ we have to connect directly to the instance
rather than the replicaset
"""
auth_is_enabled = None
connection_params = {}
connection_params['host'] = module.params['login_host']
connection_params['port'] = module.params['login_port']
connection_params['directConnection'] = True # Need to do this for 3.12.* as well
if int(PyMongoVersion[0]) >= 4: # we need to connect directly to the instance
connection_params['directConnection'] = True
else:
if 'replica_set' in module.params and module.params['replica_set'] is not None:
connection_params['replicaset'] = module.params['replica_set']
if module.params['ssl']:
connection_params = ssl_connection_options(connection_params, module)
connection_params = rename_ssl_option_for_pymongo4(connection_params)
try:
myclient = MongoClient(**connection_params)
myclient['admin'].command('listDatabases', 1.0)
auth_is_enabled = False
except Exception as excep:
if hasattr(excep, 'code') and excep.code in [13]:
auth_is_enabled = True
if auth_is_enabled is None: # if this is still none we have a problem
module.fail_json(msg='Unable to determine if auth is enabled: {0}'.format(traceback.format_exc()))
finally:
myclient.close()
return auth_is_enabled
def mongo_auth(module, client, directConnection=False):
"""
TODO: This function was extracted from code from the mongodb_replicaset module.
We should refactor other modules to use this where appropriate. - DONE?
@module - The calling Ansible module
@client - The MongoDB connection object
"""
login_user = module.params['login_user']
login_password = module.params['login_password']
login_database = module.params['login_database']
fail_msg = None # Our test code had issues with multiple exit points with fail_json
crypt_flag = 'ssl'
if 'tls' in module.params:
crypt_flag = 'tls'
if login_user is None and login_password is None:
mongocnf_creds = load_mongocnf()
if mongocnf_creds is not False:
login_user = mongocnf_creds['user']
login_password = mongocnf_creds['password']
elif not all([login_user, login_password]) and module.params[crypt_flag] is False:
fail_msg = "When supplying login arguments, both 'login_user' and 'login_password' must be provided"
if 'create_for_localhost_exception' not in module.params and fail_msg is None:
try:
if is_auth_enabled(module):
if login_user is not None and login_password is not None:
if int(PyMongoVersion[0]) < 4: # pymongo < 4
client.admin.authenticate(login_user, login_password, source=login_database)
else: # pymongo >= 4. There's no authenticate method in pymongo 4.0. Recreate the connection object
client = get_mongodb_client(module, login_user, login_password, login_database, directConnection=directConnection)
else:
fail_msg = 'No credentials to authenticate'
except Exception as excep:
fail_msg = 'unable to connect to database: %s' % to_native(excep)
# Get server version:
if fail_msg is None:
srv_version = check_srv_version(module, client)
check_driver_compatibility(module, client, srv_version)
elif fail_msg is None: # this is the mongodb_user module
if login_user is not None and login_password is not None:
if int(PyMongoVersion[0]) < 4: # pymongo < 4
client.admin.authenticate(login_user, login_password, source=login_database)
else: # pymongo >= 4
client = get_mongodb_client(module, login_user, login_password, login_database, directConnection=directConnection)
# Get server version:
srv_version = check_srv_version(module, client)
check_driver_compatibility(module, client, srv_version)
elif (PyMongoVersion.startswith('3.12') or int(PyMongoVersion[0]) > 4) \
or module.params['strict_compatibility'] is False:
if module.params['database'] not in ["admin", "$external"]:
fail_msg = 'The localhost login exception only allows the first admin account to be created'
# else: this has to be the first admin user added
if fail_msg:
module.fail_json(msg=fail_msg)
return client
def member_dicts_different(conf, member_config):
'''
Returns if there is a difference in the replicaset configuration that we care about
@con - The current MongoDB Replicaset configure document
@member_config - The member dict config provided by the module. List of dicts
'''
current_member_config = conf['members']
member_config_defaults = {
"arbiterOnly": False,
"buildIndexes": True,
"hidden": False,
"priority": {"nonarbiter": 1.0, "arbiter": 0},
"tags": {},
"secondardDelaySecs": 0,
"votes": 1
}
different = False
msg = "None"
current_member_hosts = []
for member in current_member_config:
current_member_hosts.append(member['host'])
member_config_hosts = []
for member in member_config:
if ':' not in member['host']: # no port supplied
member_config_hosts.append(member['host'] + ":27017")
else:
member_config_hosts.append(member['host'])
if sorted(current_member_hosts) != sorted(member_config_hosts): # compare if members are the same
different = True
msg = "hosts different"
else: # Compare dict key to see if votes, tags etc have changed. We also default value if key is not specified
for host in current_member_hosts:
member_index = next((index for (index, d) in enumerate(current_member_config) if d["host"] == host), None)
new_member_index = next((index for (index, d) in enumerate(member_config) if d["host"] == host), None)
for config_item in member_config_defaults:
if config_item != "priority":
if current_member_config[member_index].get(config_item, member_config_defaults[config_item]) != \
member_config[new_member_index].get(config_item, member_config_defaults[config_item]):
different = True
msg = "var different {0} {1} {2}".format(config_item,
current_member_config[member_index].get(config_item, member_config_defaults[config_item]),
member_config[new_member_index].get(config_item, member_config_defaults[config_item]))
break
else: # priority a special case
role = "nonarbiter"
if current_member_config[member_index]["arbiterOnly"]:
role = "arbiter"
if current_member_config[member_index][config_item] != \
member_config[new_member_index].get(config_item, member_config_defaults[config_item][role]):
different = True
msg = "var different {0}".format(config_item)
break
return different # , msg
def lists_are_different(list1, list2):
diff = False
if sorted(list1) != sorted(list2):
diff = True
return diff

View File

@@ -0,0 +1,452 @@
#!/usr/bin/python
# Copyright: (c) 2020, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_balancer
short_description: Manages the MongoDB Sharded Cluster Balancer.
description:
- Manages the MongoDB Sharded Cluster Balancer.
- Start or stop the balancer.
- Adjust the cluster chunksize.
- Enable or disable autosplit.
- Add or remove a balancer window.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
autosplit:
description:
- Disable or enable the autosplit flag in the config.settings collection.
required: false
type: bool
chunksize:
description:
- Control the size of chunks in the sharded cluster.
- Value should be given in MB.
required: false
type: int
state:
description:
- Manage the Balancer for the Cluster
required: false
type: str
choices:
- "started"
- "stopped"
default: "started"
mongos_process:
description:
- Provide a custom name for the mongos process.
- Most users can ignore this setting.
required: false
type: str
default: "mongos"
window:
description:
- Schedule the balancer window.
- Provide the following dictionary keys start, stop, state
- The state key should be "present" or "absent".
- The start and stop keys are ignored when state is "absent".
- start and stop should be strings in "HH:MM" format indicating the time bounds of the window.
type: raw
required: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Start the balancer
community.mongodb.mongodb_balancer:
state: started
- name: Stop the balancer and disable autosplit
community.mongodb.mongodb_balancer:
state: stopped
autosplit: false
- name: Enable autosplit
community.mongodb.mongodb_balancer:
autosplit: true
- name: Change the default chunksize to 128MB
community.mongodb.mongodb_balancer:
chunksize: 128
- name: Add or update a balancing window
community.mongodb.mongodb_balancer:
window:
start: "23:00"
stop: "06:00"
state: "present"
- name: Remove a balancing window
community.mongodb.mongodb_balancer:
window:
state: "absent"
'''
RETURN = r'''
changed:
description: Whether the balancer state or autosplit changed.
returned: success
type: bool
old_balancer_state:
description: The previous state of the balancer
returned: When balancer state is changed
type: str
new_balancer_state:
description: The new state of the balancer.
returned: When balancer state is changed
type: str
old_autosplit:
description: The previous state of autosplit.
returned: When autosplit is changed.
type: str
new_autosplit:
description: The new state of autosplit.
returned: When autosplit is changed.
type: str
old_chunksize:
description: The previous value for chunksize.
returned: When chunksize is changed.
type: int
new_chunksize:
description: The new value for chunksize.
returned: When chunksize is changed.
type: int
msg:
description: A short description of what happened.
returned: failure
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def get_balancer_state(client):
'''
Gets the state of the MongoDB balancer. The config.settings collection does
not exist until the balancer has been started for the first time
{ "_id" : "balancer", "mode" : "full", "stopped" : false }
{ "_id" : "autosplit", "enabled" : true }
'''
balancer_state = None
result = client["config"].settings.find_one({"_id": "balancer"})
if not result:
balancer_state = "stopped"
else:
if result['stopped'] is False:
balancer_state = "started"
else:
balancer_state = "stopped"
return balancer_state
def stop_balancer(client):
'''
Stops MongoDB balancer
'''
cmd_doc = OrderedDict([
('balancerStop', 1),
('maxTimeMS', 60000)
])
client['admin'].command(cmd_doc)
time.sleep(1)
def start_balancer(client):
'''
Starts MongoDB balancer
'''
cmd_doc = OrderedDict([
('balancerStart', 1),
('maxTimeMS', 60000)
])
client['admin'].command(cmd_doc)
time.sleep(1)
def enable_autosplit(client):
client["config"].settings.update_one({"_id": "autosplit"},
{"$set": {"enabled": True}},
upsert=True)
def disable_autosplit(client):
client["config"].settings.update_one({"_id": "autosplit"},
{"$set": {"enabled": False}},
upsert=True)
def get_autosplit(client):
autosplit = False
result = client["config"].settings.find_one({"_id": "autosplit"})
if result is not None:
autosplit = result['enabled']
return autosplit
def get_chunksize(client):
'''
Default chunksize is 64MB
'''
chunksize = None
result = client["config"].settings.find_one({"_id": "chunksize"})
if not result:
chunksize = 64
else:
chunksize = result['value']
return chunksize
def set_chunksize(client, chunksize):
client["config"].settings.update_one({"_id": "chunksize"},
{"$set": {"value": chunksize}},
upsert=True)
def set_balancing_window(client, start, stop):
s = False
result = client["config"].settings.update_one({"_id": "balancer"},
{"$set": {
"activeWindow": {
"start": start,
"stop": stop}}},
upsert=True)
if result.modified_count == 1 or result.upserted_id is not None:
s = True
return s
def remove_balancing_window(client):
s = False
result = client["config"].settings.update_one({"_id": "balancer"},
{"$unset": {"activeWindow": True}})
if result.modified_count == 1:
s = True
return s
def balancing_window(client, start, stop):
s = False
if start is not None and stop is not None:
result = client["config"].settings.find_one({"_id": "balancer",
"activeWindow.start": start,
"activeWindow.stop": stop})
else:
result = client["config"].settings.find_one({"_id": "balancer", "activeWindow": {"$exists": True}})
if result:
s = True
return s
def validate_window(window, module):
if window is not None:
if 'state' not in window.keys():
module.fail_json(msg="Balancing window state must be specified")
elif window['state'] not in ['present', 'absent']:
module.fail_json(msg="Balancing window state must be present or absent")
elif window['state'] == "present" \
and ("start" not in window.keys()
or "stop" not in window.keys()):
module.fail_json(msg="Balancing window start and stop values must be specified")
return True
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
autosplit=dict(type='bool', default=None),
chunksize=dict(type='int', default=None),
mongos_process=dict(type='str', required=False, default="mongos"),
state=dict(type='str', default="started", choices=["started", "stopped"]),
window=dict(type='raw', default=None)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
login_host = module.params['login_host']
login_port = module.params['login_port']
balancer_state = module.params['state']
autosplit = module.params['autosplit']
chunksize = module.params['chunksize']
mongos_process = module.params['mongos_process']
window = module.params['window']
# Validate window
validate_window(window, module)
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
changed = False
cluster_balancer_state = None
cluster_autosplit = None
cluster_chunksize = None
old_balancer_state = None
new_balancer_state = None
old_autosplit = None
new_autosplit = None
old_chunksize = None
new_chunksize = None
try:
if client["admin"].command("serverStatus")["process"] != mongos_process:
module.fail_json(msg="Process running on {0}:{1} is not a {2}".format(login_host, login_port, mongos_process))
cluster_balancer_state = get_balancer_state(client)
if autosplit is not None:
cluster_autosplit = get_autosplit(client)
if chunksize is not None:
cluster_chunksize = get_chunksize(client)
if module.check_mode:
if balancer_state != cluster_balancer_state:
old_balancer_state = cluster_balancer_state
new_balancer_state = balancer_state
changed = True
if (autosplit is not None
and autosplit != cluster_autosplit):
old_autosplit = cluster_autosplit
new_autosplit = autosplit
changed = True
if (chunksize is not None
and chunksize != cluster_chunksize):
old_chunksize = cluster_chunksize
new_chunksize = chunksize
changed = True
if window is not None:
if balancing_window(client, window.get('start'), window.get('stop')):
if window['state'] == "present":
pass
else:
changed = True
else:
if window['state'] == "present":
changed = True
else:
pass
else:
if balancer_state is not None \
and balancer_state != cluster_balancer_state:
if balancer_state == "started":
start_balancer(client)
old_balancer_state = cluster_balancer_state
new_balancer_state = get_balancer_state(client)
changed = True
else:
stop_balancer(client)
old_balancer_state = cluster_balancer_state
new_balancer_state = get_balancer_state(client)
changed = True
if autosplit is not None \
and autosplit != cluster_autosplit:
if autosplit:
enable_autosplit(client)
old_autosplit = cluster_autosplit
new_autosplit = autosplit
changed = True
else:
disable_autosplit(client)
old_autosplit = cluster_autosplit
new_autosplit = autosplit
changed = True
if (chunksize is not None
and chunksize != cluster_chunksize):
set_chunksize(client, chunksize)
old_chunksize = cluster_chunksize
new_chunksize = chunksize
changed = True
if window is not None:
if balancing_window(client, window.get('start'), window.get('stop')):
if window['state'] == "present":
pass
else:
remove_balancing_window(client)
changed = True
else:
if window['state'] == "present":
set_balancing_window(client,
window['start'],
window['stop'])
changed = True
else:
pass
except Exception as excep:
result["msg"] = "An error occurred: {0}".format(excep)
result['changed'] = changed
if old_balancer_state is not None:
result['old_balancer_state'] = old_balancer_state
result['new_balancer_state'] = new_balancer_state
if old_autosplit is not None:
result['old_autosplit'] = old_autosplit
result['new_autosplit'] = new_autosplit
if old_chunksize is not None:
result['old_chunksize'] = old_chunksize
result['new_chunksize'] = new_chunksize
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,405 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Rhys Campbell (@rhysmeister) <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_index
short_description: Creates or drops indexes on MongoDB collections.
description:
- Creates or drops indexes on MongoDB collections.
- Supports multiple index options, i.e. unique, sparse and partial.
- Validates existence of indexes by name only.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
indexes:
description:
- List of indexes to create or drop
type: list
elements: raw
required: yes
replica_set:
description:
- Replica set to connect to (automatically connects to primary for writes).
type: str
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+.
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Create a single index on a collection
community.mongodb.mongodb_index:
login_user: admin
login_password: secret
indexes:
- database: mydb
collection: test
keys:
- username: 1
last_login: -1
options:
name: myindex
state: present
- name: Drop an index on a collection
community.mongodb.mongodb_index:
login_user: admin
login_password: secret
indexes:
- database: mydb
collection: test
options:
name: myindex
state: absent
- name: Create multiple indexes
community.mongodb.mongodb_index:
login_user: admin
login_password: secret
indexes:
- database: mydb
collection: test
keys:
- username: 1
last_login: -1
options:
name: myindex
state: present
- database: mydb
collection: test
keys:
- email: 1
last_login: -1
options:
name: myindex2
state: present
- name: Add a unique index
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "rhys"
keys:
username: 1
options:
name: myuniqueindex
unique: true
state: present
- name: Add a ttl index
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "rhys"
keys:
created: 1
options:
name: myttlindex
expireAfterSeconds: 3600
state: present
- name: Add a sparse index
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "rhys"
keys:
last_login: -1
options:
name: mysparseindex
sparse: true
state: present
- name: Add a partial index
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "rhys"
keys:
last_login: -1
options:
name: mypartialindex
partialFilterExpression:
rating:
$gt: 5
state: present
- name: Add a index in the background (background option is deprecated from 4.2+)
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "rhys"
options:
name: idxbackground
keys:
username: -1
backgroud: true
state: present
- name: Check creating 5 index all with multiple options specified
community.mongodb.mongodb_index:
login_port: 27017
login_user: admin
login_password: secret
login_database: "admin"
indexes:
- database: "test"
collection: "indextest"
options:
name: "idx_unq_username"
unique: true
keys:
username: -1
state: present
- database: "test"
collection: "indextest"
options:
name: "idx_last_login"
sparse: true
keys:
last_login: -1
state: present
- database: "test"
collection: "indextest"
options:
name: "myindex"
keys:
first_name: 1
last_name: -1
city: 1
state: present
- database: "test"
collection: partialtest
options:
name: "idx_partialtest"
partialFilterExpression:
rating:
$gt: 5
keys:
rating: -1
title: 1
state: present
- database: "test"
collection: "wideindex"
options:
name: "mywideindex"
keys:
email: -1
username: 1
first_name: 1
last_name: 1
dob: -1
city: 1
last_login: -1
review_count: 1
rating_count: 1
last_post: -1
state: present
'''
RETURN = r'''
indexes_created:
description: List of indexes created.
returned: always
type: list
sample: ["myindex", "myindex2"]
indexes_dropped:
description: List of indexes dropped.
returned: always
type: list
sample: ["myindex", "myindex2"]
changed:
description: Indicates the module has changed something.
returned: When the module has changed something.
type: bool
failed:
description: Indicates the module has failed.
returned: When the module has encountered an error.
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
PYMONGO_IMP_ERR,
pymongo_found,
index_exists,
create_index,
drop_index,
mongo_auth,
get_mongodb_client,
)
def validate_module(module):
'''
Runs validation rules specific the mongodb_index module
'''
required_index_keys = [
"database",
"collection",
"options",
"state",
]
indexes = module.params['indexes']
if len(indexes) == 0:
module.fail_json(msg="One or more indexes must be specified")
if not all(isinstance(i, dict) for i in indexes):
module.fail_json(msg="Indexes must be supplied as dictionaries")
# Ensure keys are present in index spec
for k in required_index_keys:
for i in indexes:
if k not in i.keys():
module.fail_json(msg="Missing required index key {0}".format(k))
# Check index subkeys look correct
for i in indexes:
if not isinstance(i["database"], str):
module.fail_json(msg="database key should be str")
elif not isinstance(i["collection"], str):
module.fail_json(msg="collection key should be str")
elif i["state"] == "present" and "keys" not in i.keys():
module.fail_json(msg="keys must be supplied when state is present")
elif i["state"] == "present" and not isinstance(i["keys"], dict):
module.fail_json(msg="keys key should be dict")
elif not isinstance(i["options"], dict):
module.fail_json(msg="options key should be dict")
elif "name" not in i["options"]:
module.fail_json(msg="The options dict must contain a name field")
elif i["state"] not in ["present", "absent"]:
module.fail_json(msg="state must be one of present or absent")
# ================
# Module execution
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
indexes=dict(type='list', elements='raw', required=True),
replica_set=dict(type='str'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
validate_module(module)
indexes = module.params['indexes']
client = get_mongodb_client(module)
client = mongo_auth(module, client)
# Pre flight checks done
indexes_created = []
indexes_dropped = []
changed = None
for i in indexes:
try:
idx = index_exists(client, i["database"], i["collection"], i["options"]["name"])
except Exception as excep:
module.fail_json(msg="Could not determine index status: {0}".format(str(excep)))
if module.check_mode:
if idx:
if i["state"] == "present":
changed = False
elif i["state"] == "absent":
indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
i["collection"],
i["options"]["name"]))
changed = True
else:
if i["state"] == "present":
indexes_created.append("{0}.{1}.{2}".format(i["database"],
i["collection"],
i["options"]["name"]))
changed = True
elif i["state"] == "absent":
changed = False
else:
if idx:
if i["state"] == "present":
changed = False
elif i["state"] == "absent":
try:
drop_index(client, i["database"], i["collection"],
i["options"]["name"])
indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
i["collection"],
i["options"]["name"]))
changed = True
except Exception as excep:
module.fail_json(msg="Error dropping index: {0}".format(str(excep)))
else:
if i["state"] == "present":
try:
create_index(client=client,
database=i["database"],
collection=i["collection"],
keys=i["keys"],
options=i["options"])
indexes_created.append("{0}.{1}.{2}".format(i["database"],
i["collection"],
i["options"]["name"]))
changed = True
except Exception as excep:
module.fail_json(msg="Error creating index: {0}".format(str(excep)))
elif i["state"] == "absent":
changed = False
module.exit_json(changed=changed,
indexes_created=indexes_created,
indexes_dropped=indexes_dropped)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,313 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_info
short_description: Gather information about MongoDB instance.
description:
- Gather information about MongoDB instance.
author: Andrew Klychkov (@Andersson007)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
filter:
description:
- Limit the collected information by comma separated string or YAML list.
- Allowable values are C(general), C(databases), C(total_size), C(parameters), C(users), C(roles).
- By default, collects all subsets.
- You can use '!' before value (for example, C(!users)) to exclude it from the information.
- If you pass including and excluding values to the filter, for example, I(filter=!general,users),
the excluding values, C(!general) in this case, will be ignored.
required: no
type: list
elements: str
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+.
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Gather all supported information
community.mongodb.mongodb_info:
login_user: admin
login_password: secret
register: result
- name: Show gathered info
debug:
msg: '{{ result }}'
- name: Gather only information about databases and their total size
community.mongodb.mongodb_info:
login_user: admin
login_password: secret
filter: databases, total_size
- name: Gather all information except parameters
community.mongodb.mongodb_info:
login_user: admin
login_password: secret
filter: '!parameters'
'''
RETURN = r'''
general:
description: General instance information.
returned: always
type: dict
sample: {"allocator": "tcmalloc", "bits": 64, "storageEngines": ["biggie"], "version": "4.2.3", "maxBsonObjectSize": 16777216}
databases:
description: Database information.
returned: always
type: dict
sample: {"admin": {"empty": false, "sizeOnDisk": 245760}, "config": {"empty": false, "sizeOnDisk": 110592}}
total_size:
description: Total size of all databases in bytes.
returned: always
type: int
sample: 397312
users:
description: User information.
returned: always
type: dict
sample: { "db": {"new_user": {"_id": "config.new_user", "mechanisms": ["SCRAM-SHA-1", "SCRAM-SHA-256"], "roles": []}}}
roles:
description: Role information.
returned: always
type: dict
sample: { "db": {"restore": {"inheritedRoles": [], "isBuiltin": true, "roles": []}}}
parameters:
description: Server parameters information.
returned: always
type: dict
sample: {"maxOplogTruncationPointsAfterStartup": 100, "maxOplogTruncationPointsDuringStartup": 100, "maxSessions": 1000000}
'''
from uuid import UUID
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.six import iteritems
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
class MongoDbInfo():
"""Class for gathering MongoDB instance information.
Args:
module (AnsibleModule): Object of AnsibleModule class.
client (pymongo): pymongo client object to interact with the database.
"""
def __init__(self, module, client):
self.module = module
self.client = client
self.admin_db = self.client.admin
self.info = {
'general': {},
'databases': {},
'total_size': {},
'parameters': {},
'users': {},
'roles': {},
}
def get_info(self, filter_):
"""Get MongoDB instance information and return it based on filter_.
Args:
filter_ (list): List of collected subsets (e.g., general, users, etc.),
when it is empty, return all available information.
"""
self.__collect()
inc_list = []
exc_list = []
if filter_:
partial_info = {}
for fi in filter_:
if fi.lstrip('!') not in self.info:
self.module.warn("filter element '%s' is not allowable, ignored" % fi)
continue
if fi[0] == '!':
exc_list.append(fi.lstrip('!'))
else:
inc_list.append(fi)
if inc_list:
for i in self.info:
if i in inc_list:
partial_info[i] = self.info[i]
else:
for i in self.info:
if i not in exc_list:
partial_info[i] = self.info[i]
return partial_info
else:
return self.info
def __collect(self):
"""Collect information."""
# Get general info:
self.info['general'] = self.client.server_info()
# Get parameters:
self.info['parameters'] = self.get_parameters_info()
# Gather info about databases and their total size:
self.info['databases'], self.info['total_size'] = self.get_db_info()
for dbname, val in iteritems(self.info['databases']):
# Gather info about users for each database:
self.info['users'].update(self.get_users_info(dbname))
# Gather info about roles for each database:
self.info['roles'].update(self.get_roles_info(dbname))
def get_roles_info(self, dbname):
"""Gather information about roles.
Args:
dbname (str): Database name to get role info from.
Returns a dictionary with role information for the given db.
"""
db = self.client[dbname]
result = db.command({'rolesInfo': 1, 'showBuiltinRoles': True})['roles']
roles_dict = {}
for elem in result:
roles_dict[elem['role']] = {}
for key, val in iteritems(elem):
if key in ['role', 'db']:
continue
roles_dict[elem['role']][key] = val
return {dbname: roles_dict}
def get_users_info(self, dbname):
"""Gather information about users.
Args:
dbname (str): Database name to get user info from.
Returns a dictionary with user information for the given db.
"""
db = self.client[dbname]
result = db.command({'usersInfo': 1})['users']
users_dict = {}
for elem in result:
users_dict[elem['user']] = {}
for key, val in iteritems(elem):
if key in ['user', 'db']:
continue
if isinstance(val, UUID):
val = val.hex
users_dict[elem['user']][key] = val
return {dbname: users_dict}
def get_db_info(self):
"""Gather information about databases.
Returns a dictionary with database information.
"""
result = self.admin_db.command({'listDatabases': 1})
total_size = int(result['totalSize'])
result = result['databases']
db_dict = {}
for elem in result:
db_dict[elem['name']] = {}
for key, val in iteritems(elem):
if key == 'name':
continue
if key == 'sizeOnDisk':
val = int(val)
db_dict[elem['name']][key] = val
return db_dict, total_size
def get_parameters_info(self):
"""Gather parameters information.
Returns a dictionary with parameters.
"""
return self.admin_db.command({'getParameter': '*'})
# ================
# Module execution
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
filter=dict(type='list', elements='str', required=False)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
filter_ = module.params['filter']
if filter_:
filter_ = [f.strip() for f in filter_]
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
# Initialize an object and start main work:
mongodb = MongoDbInfo(module, client)
module.exit_json(changed=False, **mongodb.get_info(filter_))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,146 @@
#!/usr/bin/python
# Copyright: (c) 2020, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_maintenance
short_description: Enables or disables maintenance mode for a secondary member.
description:
- Enables or disables maintenance mode for a secondary member.
- Wrapper around the replSetMaintenance command.
- Performs no actions against a PRIMARY member.
- When enabled SECONDARY members will not service reads.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
maintenance:
description: Enable or disable maintenance mode.
type: bool
default: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Enable maintenance mode
community.mongodb.mongodb_maintenance:
maintenance: true
- name: Disable maintenance mode
community.mongodb.mongodb_maintenance:
maintenance: false
'''
RETURN = r'''
changed:
description: Whether the member was placed into maintenance mode or not.
returned: success
type: bool
msg:
description: A short description of what happened.
returned: success
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
member_state,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def put_in_maint_mode(client):
client['admin'].command('replSetMaintenance', True)
def remove_maint_mode(client):
client['admin'].command('replSetMaintenance', False)
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
maintenance=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
maintenance = module.params['maintenance']
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
state = member_state(client)
if state == "PRIMARY":
result["msg"] = "no action taken as member state was PRIMARY"
elif state == "SECONDARY":
if maintenance:
if module.check_mode:
result["changed"] = True
result["msg"] = "member was placed into maintenance mode"
else:
put_in_maint_mode(client)
result["changed"] = True
result["msg"] = "member was placed into maintenance mode"
else:
result["msg"] = "No action taken as maintenance parameter is false and member state is SECONDARY"
elif state == "RECOVERING":
if maintenance:
result["msg"] = "no action taken as member is already in a RECOVERING state"
else:
if module.check_mode:
result["changed"] = True
result["msg"] = "the member was removed from maintenance mode"
else:
remove_maint_mode(client)
result["changed"] = True
result["msg"] = "the member was removed from maintenance mode"
else:
result["msg"] = "no action taken as member state was {0}".format(state)
except Exception as excep:
module.fail_json(msg='module encountered an error: %s' % to_native(excep))
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,197 @@
#!/usr/bin/python
# Copyright: (c) 2021, Rhys Campbell rhyscampbell@blueiwn.ch
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_monitoring
short_description: Manages the free monitoring feature.
description:
- Manages the free monitoring feature.
- Optionally return the monitoring url.
author: Rhys Campbell (@rhysmeister)
version_added: "1.3.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
state:
description: Manage the free monitoring feature.
type: str
choices:
- "started"
- "stopped"
default: "started"
return_url:
description: When true return the monitoring url if available.
type: bool
default: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Enable monitoring
community.mongodb.mongodb_monitoring:
state: "started"
- name: Disable monitoring
community.mongodb.mongodb_monitoring:
state: "stopped"
- name: Enable monitoring and return the monitoring url
community.mongodb_monitoring:
state: "started"
return_url: "yes"
'''
RETURN = r'''
changed:
description: Whether the monitoring status changed.
returned: success
type: bool
msg:
description: A short description of what happened.
returned: success
type: str
failed:
description: If something went wrong
returned: failed
type: bool
url:
description: The MongoDB instance Monitoring url.
returned: When requested and available.
type: str
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
PYMONGO_IMP_ERR,
pymongo_found,
mongo_auth,
get_mongodb_client,
)
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def stop_monitoring(client):
'''
Stops MongoDB Free Monitoring
'''
cmd_doc = OrderedDict([('setFreeMonitoring', 1),
('action', 'disable')])
client['admin'].command(cmd_doc)
def start_monitoring(client):
'''
Stops MongoDB Free Monitoring
'''
cmd_doc = OrderedDict([('setFreeMonitoring', 1),
('action', 'enable')])
client['admin'].command(cmd_doc)
def get_monitoring_status(client):
'''
Gets the state of MongoDB Monitoring.
N.B. If Monitoring has never been enabled the
free_monitoring record in admin.system.version
will not yet exist.
'''
monitoring_state = None
url = None
result = client["admin"]['system.version'].find_one({"_id": "free_monitoring"})
if not result:
monitoring_state = "stopped"
else:
url = result["informationalURL"]
if result["state"] == "enabled":
monitoring_state = "started"
else:
monitoring_state = "stopped"
return monitoring_state, url
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
state=dict(type='str', default='started', choices=['started', 'stopped']),
return_url=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
state = module.params['state']
return_url = module.params['return_url']
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
current_monitoring_state, url = get_monitoring_status(client)
result = {}
if state == "started":
if current_monitoring_state == "started":
result['changed'] = False
result['msg'] = "Free monitoring is already started"
else:
if module.check_mode is False:
start_monitoring(client)
result['changed'] = True
result['msg'] = "Free monitoring has been started"
else:
if current_monitoring_state == "started":
if module.check_mode is False:
stop_monitoring(client)
result['changed'] = True
result['msg'] = "Free monitoring has been stopped"
else:
result['changed'] = False
result['msg'] = "Free monitoring is already stopped"
if return_url and url:
result['url'] = url
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,191 @@
#!/usr/bin/python
# Copyright: (c) 2020, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_oplog
short_description: Resizes the MongoDB oplog.
description:
- Resizes the MongoDB oplog.
- This module should only be used with MongoDB 3.6 and above.
- Old MongoDB versions should use an alternative method.
- Consult U(https://docs.mongodb.com/manual/tutorial/change-oplog-size) for further info.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
oplog_size_mb:
description:
- New size of the oplog in MB.
type: int
required: true
compact:
description:
- Runs compact against the oplog.rs collection in the local database to reclaim disk space.
- Performs no actions against PRIMARY members.
- The MongoDB user must have the compact role on the local database for this feature to work.
type: bool
default: false
required: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Resize oplog to 16 gigabytes, or 16000 megabytes
community.mongodb.mongodb_oplog:
oplog_size_mb: 16000
- name: Resize oplog to 8 gigabytes and compact secondaries to reclaim space
community.mongodb.mongodb_oplog:
oplog_size_mb: 8000
compact: true
'''
RETURN = r'''
changed:
description: Whether the member oplog was modified.
returned: success
type: bool
compacted:
description: Whether the member oplog was compacted.
returned: success
type: bool
msg:
description: A short description of what happened.
returned: success
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
member_state,
mongo_auth,
check_srv_version,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def get_olplog_size(client):
return int(client["local"].command("collStats", "oplog.rs")["maxSize"]) / 1024 / 1024
def set_oplog_size(client, oplog_size_mb):
cmd_doc = OrderedDict([
('replSetResizeOplog', 1),
('size', oplog_size_mb)
])
client["admin"].command(cmd_doc)
def compact_oplog(client):
client["local"].command("compact", "oplog.rs")
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
compact=dict(type='bool', default=False),
oplog_size_mb=dict(type='int', required=True),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
oplog_size_mb = float(module.params['oplog_size_mb']) # MongoDB 4.4 inists on a real
compact = module.params['compact']
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
current_oplog_size = get_olplog_size(client)
except Exception as excep:
module.fail_json(msg='Unable to get current oplog size: %s' % to_native(excep))
if oplog_size_mb == current_oplog_size:
result["msg"] = "oplog_size_mb is already {0} mb".format(int(oplog_size_mb))
result["compacted"] = False
else:
try:
state = member_state(client)
except Exception as excep:
module.fail_json(msg='Unable to get member state: %s' % to_native(excep))
if module.check_mode:
result["changed"] = True
result["msg"] = "oplog has been resized from {0} mb to {1} mb".format(int(current_oplog_size),
int(oplog_size_mb))
if state == "SECONDARY" and compact and current_oplog_size > oplog_size_mb:
result["compacted"] = True
else:
result["compacted"] = False
else:
try:
set_oplog_size(client, oplog_size_mb)
result["changed"] = True
result["msg"] = "oplog has been resized from {0} mb to {1} mb".format(int(current_oplog_size),
int(oplog_size_mb))
except Exception as excep:
module.fail_json(msg='Unable to set oplog size: %s' % to_native(excep))
if state == "SECONDARY" and compact and current_oplog_size > oplog_size_mb:
try:
compact_oplog(client)
result["compacted"] = True
except Exception as excep:
module.fail_json(msg='Error compacting member oplog: %s' % to_native(excep))
else:
result["compacted"] = False
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,144 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2016, Loic Blot <loic.blot@unix-experience.fr>
# Sponsored by Infopro Digital. http://www.infopro-digital.com/
# Sponsored by E.T.A.I. http://www.etai.fr/
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_parameter
short_description: Change an administrative parameter on a MongoDB server
description:
- Change an administrative parameter on a MongoDB server.
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replica set to connect to (automatically connects to primary for writes).
type: str
param:
description:
- MongoDB administrative parameter to modify.
type: str
required: true
value:
description:
- MongoDB administrative parameter value to set.
type: str
required: true
param_type:
description:
- Define the type of parameter value.
default: str
type: str
choices: [int, str]
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+.
- This can be installed using pip or the OS package manager.
- See also U(http://api.mongodb.org/python/current/installation.html)
requirements: [ "pymongo" ]
author: "Loic Blot (@nerzhul)"
'''
EXAMPLES = r'''
- name: Set MongoDB syncdelay to 60 (this is an int)
community.mongodb.mongodb_parameter:
param: syncdelay
value: 60
param_type: int
'''
RETURN = r'''
before:
description: value before modification
returned: success
type: str
after:
description: value after modification
returned: success
type: str
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
OperationFailure,
get_mongodb_client,
)
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
replica_set=dict(default=None),
param=dict(required=True),
value=dict(required=True),
param_type=dict(default="str", choices=['str', 'int'])
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
param = module.params['param']
param_type = module.params['param_type']
value = module.params['value']
# Verify parameter is coherent with specified type
try:
if param_type == 'int':
value = int(value)
except ValueError:
module.fail_json(msg="value '%s' is not %s" % (value, param_type))
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
db = client.admin
try:
after_value = db.command("setParameter", **{param: value})
except OperationFailure as e:
module.fail_json(msg="unable to change parameter: %s" % to_native(e), exception=traceback.format_exc())
if "was" not in after_value:
module.exit_json(changed=True, msg="Unable to determine old value, assume it changed.")
else:
module.exit_json(changed=(value != after_value["was"]), before=after_value["was"],
after=value)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,611 @@
#!/usr/bin/python
# Copyright: (c) 2018, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_replicaset
short_description: Initialises a MongoDB replicaset.
description:
- Initialises a MongoDB replicaset in a new deployment.
- Validates the replicaset name for existing deployments.
- Advanced replicaset member configuration possible (see examples).
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replicaset name.
type: str
default: rs0
members:
description:
- Yaml list consisting of the replicaset members.
- Csv string will also be accepted i.e. mongodb1:27017,mongodb2:27017,mongodb3:27017.
- A dictionary can also be used to specify advanced replicaset member options.
- If a port number is not provided then 27017 is assumed.
type: list
elements: raw
validate:
description:
- Performs some basic validation on the provided replicaset config.
type: bool
default: yes
arbiter_at_index:
description:
- Identifies the position of the member in the array that is an arbiter.
type: int
chaining_allowed:
description:
- When I(settings.chaining_allowed=true), the replicaset allows secondary members to replicate from other
secondary members.
- When I(settings.chaining_allowed=false), secondaries can replicate only from the primary.
type: bool
default: yes
heartbeat_timeout_secs:
description:
- Number of seconds that the replicaset members wait for a successful heartbeat from each other.
- If a member does not respond in time, other members mark the delinquent member as inaccessible.
- The setting only applies when using I(protocol_version=0). When using I(protocol_version=1) the relevant
setting is I(settings.election_timeout_millis).
type: int
default: 10
election_timeout_millis:
description:
- The time limit in milliseconds for detecting when a replicaset's primary is unreachable.
type: int
default: 10000
protocol_version:
description: Version of the replicaset election protocol.
type: int
choices: [ 0, 1 ]
default: 1
reconfigure:
description:
- This feature is currently experimental. Please test your scenario thoroughly.
- Consult the integration test file for supported scenarios - \
[Integration tests](https://github.com/ansible-collections/community.mongodb/tree/master/tests/integration/targets/mongodb_replicaset/tasks). \
See files prefixed with 330.
- Whether to perform replicaset reconfiguration actions.
- Only relevant when the replicaset already exists.
- Only one member should be removed or added per invocation.
- Members should be specific as either all strings or all dicts when reconfiguring.
- Currently no support for replicaset settings document changes.
type: bool
default: false
force:
description:
- Only relevant when reconfigure = true.
- Specify true to force the available replica set members to accept the new configuration.
- Force reconfiguration can result in unexpected or undesired behavior, including rollback of "majority" committed writes.
type: bool
default: false
max_time_ms:
description:
- Specifies a cumulative time limit in milliseconds for processing the replicaset reconfiguration.
type: int
default: null
debug:
description:
- Add additonal info for debug.
type: bool
default: false
cluster_cmd:
description:
- Command the module should use to obtain information about the MongoDB node we are connecting to.
type: str
choices:
- isMaster
- hello
default: hello
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
# Create a replicaset called 'rs0' with the 3 provided members
- name: Ensure replicaset rs0 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_user: admin
login_password: admin
replica_set: rs0
members:
- mongodb1:27017
- mongodb2:27017
- mongodb3:27017
when: groups.mongod.index(inventory_hostname) == 0
# Create two single-node replicasets on the localhost for testing
- name: Ensure replicaset rs0 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3001
login_user: admin
login_password: secret
login_database: admin
replica_set: rs0
members: localhost:3001
validate: no
- name: Ensure replicaset rs1 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3002
login_user: admin
login_password: secret
login_database: admin
replica_set: rs1
members: localhost:3002
validate: no
- name: Create a replicaset and use a custom priority for each member
community.mongodb.mongodb_replicaset:
login_host: localhost
login_user: admin
login_password: admin
replica_set: rs0
members:
- host: "localhost:3001"
priority: 1
- host: "localhost:3002"
priority: 0.5
- host: "localhost:3003"
priority: 0.5
when: groups.mongod.index(inventory_hostname) == 0
- name: Create replicaset rs1 with options and member tags
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3001
login_database: admin
replica_set: rs1
members:
- host: "localhost:3001"
priority: 1
tags:
dc: "east"
usage: "production"
- host: "localhost:3002"
priority: 1
tags:
dc: "east"
usage: "production"
- host: "localhost:3003"
priority: 0
hidden: true
slaveDelay: 3600
tags:
dc: "west"
usage: "reporting"
- name: Replicaset with one arbiter node (mongodb3 - index is zero-based)
community.mongodb.mongodb_replicaset:
login_user: admin
login_password: admin
replica_set: rs0
members:
- mongodb1:27017
- mongodb2:27017
- mongodb3:27017
arbiter_at_index: 2
when: groups.mongod.index(inventory_hostname) == 0
- name: Add a new member to a replicaset - Safe for pre-5.0 consult documentation - https://docs.mongodb.com/manual/tutorial/expand-replica-set/
block:
- name: Create replicaset with module - with dicts
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3003
- name: Wait for the replicaset to stabilise
community.mongodb.mongodb_status:
replica_set: "rs0"
poll: 5
interval: 10
- name: Remove a member from the replicaset
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- name: Wait for the replicaset to stabilise after member removal
community.mongodb.mongodb_status:
replica_set: "rs0"
validate: minimal
poll: 5
interval: 10
- name: Add a member to the replicaset
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3004
hidden: true
votes: 0
priority: 0
- name: Wait for the replicaset to stabilise after member addition
community.mongodb.mongodb_status:
replica_set: "rs0"
validate: minimal
poll: 5
interval: 30
- name: Reconfigure the replicaset - Make member 3004 a normal voting member
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3004
hidden: false
votes: 1
priority: 1
- name: Wait for the replicaset to stabilise
community.mongodb.mongodb_status:
replica_set: "rs0"
poll: 5
interval: 30
'''
RETURN = r'''
mongodb_replicaset:
description: The name of the replicaset that has been created.
returned: success
type: str
reconfigure:
description: If a replicaset reconfiguration occured.
returned: On rpelicaset reconfiguration
type: bool
'''
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
member_dicts_different,
lists_are_different,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def get_replicaset_config(client):
conf = client.admin.command({'replSetGetConfig': 1})
return conf['config']
def get_member_names(client):
conf = get_replicaset_config(client)
members = []
for member in conf['members']:
members.append(member['host'])
return members
def modify_members(module, config, members):
"""
Modifies the members section of the config document as appropriate.
@module - Ansible module object
@config - Replicaset config document from MongoDB
@members - Members config from module
"""
try: # refactor repeated code
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
new_member_config = [] # the list of dicts containing the members for the replicaset configuration document
existing_members = [] # members that are staying in the config
max_id = 0
if all(isinstance(member, str) for member in members):
for current_member in config['members']:
if current_member["host"] in members:
new_member_config.append(current_member)
existing_members.append(current_member["host"])
if current_member["_id"] > max_id:
max_id = current_member["_id"]
member_additions = list(set(members) - set(existing_members))
if len(member_additions) > 0:
for member in member_additions:
if ':' not in member: # No port supplied. Assume 27017
member += ":27017"
new_member_config.append(OrderedDict([("_id", max_id + 1), ("host", member)]))
max_id += 1
config["members"] = new_member_config
elif all(isinstance(member, dict) for member in members):
# We need to put the _id values in into the matching document and generate them for new hosts
# TODO: https://docs.mongodb.com/manual/reference/replica-configuration/#mongodb-rsconf-rsconf.members-n-._id
# Maybe we can add a new member id parameter value, stick with the incrementing for now
# Perhaps even save this in the mongodb instance?
# first get all the existing members of the replicaset
new_member_config = []
existing_members = {}
matched_members = [] # members that have been supplied by the moduel and matched with existing members
max_id = 0
for member in config["members"]:
existing_members[member["host"]] = member["_id"]
if member["_id"] > max_id:
max_id = member["_id"]
# append existing members with the appropriate _id
for member in members:
if member["host"] in existing_members:
member["_id"] = existing_members[member["host"]]
matched_members.append(member["host"])
new_member_config.append(member)
for member in members:
if member["host"] not in matched_members: # new member , append and increment id
max_id = max_id + 1
member["_id"] = max_id
new_member_config.append(member)
config["members"] = new_member_config
else:
module.fail_json(msg="All items in members must be either of type dict of str")
return config
def replicaset_reconfigure(module, client, config, force, max_time_ms):
config['version'] += 1
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
cmd_doc = OrderedDict([("replSetReconfig", config),
("force", force)])
if max_time_ms is not None:
cmd_doc.update({"maxTimeMS": max_time_ms})
client.admin.command(cmd_doc)
# return result
def replicaset_find(client, cluster_cmd):
"""Check if a replicaset exists.
Args:
client (cursor): Mongodb cursor on admin database.
cluster_cmd (str): Either isMaster or hello
Returns:
str: when the node is a member of a replicaset , False otherwise.
"""
doc = client['admin'].command(cluster_cmd)
if 'setName' in doc:
return str(doc['setName'])
return False
def replicaset_add(module, client, replica_set, members, arbiter_at_index, protocol_version,
chaining_allowed, heartbeat_timeout_secs, election_timeout_millis):
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
members_dict_list = []
index = 0
settings = {
"chainingAllowed": bool(chaining_allowed),
}
if protocol_version == 0:
settings['heartbeatTimeoutSecs'] = heartbeat_timeout_secs
else:
settings['electionTimeoutMillis'] = election_timeout_millis
for member in members:
if isinstance(member, str):
if ':' not in member: # No port supplied. Assume 27017
member += ":27017"
members_dict_list.append(OrderedDict([("_id", int(index)), ("host", member)]))
if index == arbiter_at_index:
members_dict_list[index]['arbiterOnly'] = True
index += 1
elif isinstance(member, dict):
hostname = member["host"]
if ':' not in hostname:
hostname += ":27017"
members_dict_list.append(OrderedDict([("_id", int(index)), ("host", hostname)]))
for key in list(member.keys()):
if key != "host":
members_dict_list[index][key] = member[key]
if index == arbiter_at_index:
members_dict_list[index]['arbiterOnly'] = True
index += 1
else:
raise ValueError("member should be a str or dict. Instead found: {0}".format(str(type(members))))
conf = OrderedDict([("_id", replica_set),
("protocolVersion", protocol_version),
("members", members_dict_list),
("settings", settings)])
try:
client["admin"].command('replSetInitiate', conf)
except Exception as excep:
raise Exception("Some problem {0} | {1}".format(str(excep), str(conf)))
def replicaset_remove(module, client, replica_set):
raise NotImplementedError
def modify_members_flow(module, client, members, result):
debug = module.params['debug']
force = module.params['force']
max_time_ms = module.params['max_time_ms']
diff = False
modified_config = None
config = None
try:
config = get_replicaset_config(client)
except Exception as excep:
module.fail_json(msg="Unable to get replicaset configuration {0}".format(excep))
if isinstance(members[0], str):
diff = lists_are_different(members, get_member_names(client))
elif isinstance(members[0], dict):
diff = member_dicts_different(config, members)
else:
module.fail_json(msg="members must be either str or dict")
if diff:
if not module.check_mode:
try:
modified_config = modify_members(module, config, members)
if debug:
result['config'] = str(config)
result['modified_config'] = str(modified_config)
replicaset_reconfigure(module, client, modified_config, force, max_time_ms)
except Exception as excep:
module.fail_json(msg="Failed reconfiguring replicaset {0}, config doc {1}".format(excep, modified_config))
result['changed'] = True
result['msg'] = "replicaset reconfigured"
else:
result['changed'] = False
return result
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
arbiter_at_index=dict(type='int'),
chaining_allowed=dict(type='bool', default=True),
election_timeout_millis=dict(type='int', default=10000),
heartbeat_timeout_secs=dict(type='int', default=10),
members=dict(type='list', elements='raw'),
protocol_version=dict(type='int', default=1, choices=[0, 1]),
replica_set=dict(type='str', default="rs0"),
validate=dict(type='bool', default=True),
reconfigure=dict(type='bool', default=False),
force=dict(type='bool', default=False),
max_time_ms=dict(type='int', default=None),
debug=dict(type='bool', default=False),
cluster_cmd=dict(type='str', choices=['isMaster', 'hello'], default='hello')
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
replica_set = module.params['replica_set']
members = module.params['members']
arbiter_at_index = module.params['arbiter_at_index']
validate = module.params['validate']
protocol_version = module.params['protocol_version']
chaining_allowed = module.params['chaining_allowed']
heartbeat_timeout_secs = module.params['heartbeat_timeout_secs']
election_timeout_millis = module.params['election_timeout_millis']
reconfigure = module.params['reconfigure']
force = module.params['force'] # TODO tidy this stuff up
max_time_ms = module.params['max_time_ms']
debug = module.params['debug']
cluster_cmd = module.params['cluster_cmd']
if validate and reconfigure is False:
if len(members) <= 2 or len(members) % 2 == 0:
module.fail_json(msg="MongoDB Replicaset validation failed. Invalid number of replicaset members.")
if arbiter_at_index is not None and len(members) - 1 < arbiter_at_index:
module.fail_json(msg="MongoDB Replicaset validation failed. Invalid arbiter index.")
result = dict(
changed=False,
replica_set=replica_set,
)
try:
client = get_mongodb_client(module, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
try:
rs = replicaset_find(client, cluster_cmd) # does not require auth
except Exception as e:
module.fail_json(msg='Unable to connect to query replicaset: %s' % to_native(e))
if isinstance(rs, str):
if replica_set == rs:
if reconfigure:
client = mongo_auth(module, client)
result = modify_members_flow(module, client, members, result)
else:
result['changed'] = False
result['replica_set'] = rs
module.exit_json(**result)
else:
module.fail_json(msg="The replica_set name of {0} does not match the expected: {1}".format(rs, replica_set))
else: # replicaset does not exist
# Some validation stuff
if len(replica_set) == 0:
module.fail_json(msg="Parameter replica_set must not be an empty string")
if module.check_mode is False:
try:
replicaset_add(module, client, replica_set, members,
arbiter_at_index, protocol_version,
chaining_allowed, heartbeat_timeout_secs,
election_timeout_millis)
result['changed'] = True
except Exception as e:
module.fail_json(msg='Unable to create replica_set: %s' % to_native(e))
else:
result['changed'] = True
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,345 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rhys Campbell (@rhysmeister) <rhyscampbell@bluewin.ch>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_schema
short_description: Manages MongoDB Document Schema Validators.
description:
- Manages MongoDB Document Schema Validators.
- Create, update and remove Validators on a collection.
- Supports the entire range of jsonSchema keywords.
- See [jsonSchema Available Keywords](https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords) for details.
author: Rhys Campbell (@rhysmeister)
version_added: "1.3.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
db:
description:
- The database to work with.
required: yes
type: str
collection:
description:
- The collection to work with.
required: yes
type: str
required:
description:
- List of fields that are required.
type: list
elements: str
properties:
description:
- Individual property specification.
type: dict
action:
description:
- The validation action for MongoDB to perform when handling invalid documents.
type: str
choices:
- "error"
- "warn"
default: "error"
level:
description:
- The validation level MongoDB should apply when updating existing documents.
type: str
choices:
- "strict"
- "moderate"
default: "strict"
replica_set:
description:
- Replicaset name.
type: str
default: null
state:
description:
- The state of the validator.
type: str
choices:
- "present"
- "absent"
default: "present"
debug:
description:
- Enable additional debugging output.
type: bool
default: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+.
requirements:
- pymongo
'''
EXAMPLES = r'''
---
- name: Require that an email address field is in every document
community.mongodb.mongodb_schema:
collection: contacts
db: rhys
required:
- email
- name: Remove a schema rule
community.mongodb.mongodb_schema:
collection: contacts
db: rhys
state: absent
- name: More advanced example using properties
community.mongodb.mongodb_schema:
collection: contacts
db: rhys
properties:
email:
maxLength: 150
minLength: 5
options:
bsonType: array
maxItems: 10
minItems: 5
uniqueItems: true
status:
bsonType: string
description: "can only be ACTIVE or DISABLED"
enum:
- ACTIVE
- DISABLED
year:
bsonType: int
description: "must be an integer from 2021 to 3020"
exclusiveMaximum: false
maximum: 3020
minimum: 2021
required:
- email
- first_name
- last_name
'''
RETURN = r'''
changed:
description: If the module caused a change.
returned: on success
type: bool
msg:
description: Status message.
returned: always
type: str
validator:
description: The validator document as read from the instance.
returned: when debug is true
type: dict
module_config:
description: The validator document as indicated by the module invocation.
returned: when debug is true
type: dict
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
import json
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def get_validator(client, db, collection):
validator = None
cmd_doc = OrderedDict([
('listCollections', 1),
('filter', {"name": collection})
])
doc = None
results = client[db].command(cmd_doc)["cursor"]["firstBatch"]
if len(results) > 0:
doc = results[0]
if doc is not None and 'options' in doc and 'validator' in doc['options']:
validator = doc['options']['validator']["$jsonSchema"]
if 'validationAction' in doc['options']:
validator['validationAction'] = doc['options']['validationAction']
if 'validationLevel' in doc['options']:
validator['validationLevel'] = doc['options']['validationLevel']
return validator
def validator_is_different(client, db, collection, required, properties, action, level):
is_different = False
validator = get_validator(client, db, collection)
if validator is not None:
if sorted(required) != sorted(validator.get('required', [])):
is_different = True
if action != validator.get('validationAction', 'error'):
is_different = True
if level != validator.get('validationLevel', 'strict'):
is_different = True
dict1 = json.dumps(properties, sort_keys=True)
dict2 = json.dumps(validator.get('properties', {}), sort_keys=True)
if dict1 != dict2:
is_different = True
else:
is_different = True
return is_different
def add_validator(client, db, collection, required, properties, action, level):
cmd_doc = OrderedDict([
('collMod', collection),
('validator', {'$jsonSchema': {"bsonType": "object",
"required": required,
"properties": properties}}),
('validationAction', action),
('validationLevel', level)
])
if collection not in client[db].list_collection_names():
client[db].create_collection(collection)
client[db].command(cmd_doc)
def remove_validator(client, db, collection):
cmd_doc = OrderedDict([
('collMod', collection),
('validator', {}),
('validationLevel', "off")
])
client[db].command(cmd_doc)
# ================
# Module execution
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
db=dict(type='str', required=True),
collection=dict(type='str', required=True),
required=dict(type='list', elements='str'),
properties=dict(type='dict', default={}),
action=dict(type='str', choices=['error', 'warn'], default="error"),
level=dict(type='str', choices=['strict', 'moderate'], default="strict"),
state=dict(type='str', choices=['present', 'absent'], default='present'),
debug=dict(type='bool', default=False),
replica_set=dict(type='str', default=None),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
required_if=[("state", "present", ("db", "collection"))]
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
db = module.params['db']
collection = module.params['collection']
required = module.params['required']
properties = module.params['properties']
action = module.params['action']
level = module.params['level']
state = module.params['state']
debug = module.params['debug']
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
result = dict(
changed=False,
)
validator = get_validator(client, db, collection)
if state == "present":
if validator is not None:
diff = validator_is_different(client, db, collection, required,
properties, action, level)
if diff:
if not module.check_mode:
add_validator(client,
db,
collection,
required,
properties,
action,
level)
result['changed'] = True
result['msg'] = "The validator was updated on the given collection"
else:
result['changed'] = False
result['msg'] = "The validator exists as configured on the given collection"
else:
if not module.check_mode:
add_validator(client,
db,
collection,
required,
properties,
action,
level)
result['changed'] = True
result['msg'] = "The validator has been added to the given collection"
elif state == "absent":
if validator is None:
result['changed'] = False
result['msg'] = "A validator does not exist on the given collection."
else:
if not module.check_mode:
remove_validator(client, db, collection)
result['changed'] = True
result['msg'] = "The validator has been removed from the given collection"
if debug:
result['validator'] = validator
result['module_config'] = {"required": required,
"properties": properties,
"validationAction": action,
"validationLevel": level}
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,305 @@
#!/usr/bin/python
# (c) 2018, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: mongodb_shard
short_description: Add or remove shards from a MongoDB Cluster
description:
- Add or remove shards from a MongoDB Cluster.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
shard:
description:
- The shard connection string.
- Should be supplied in the form <replicaset>/host:port as detailed in U(https://docs.mongodb.com/manual/tutorial/add-shards-to-shard-cluster/).
- For example rs0/example1.mongodb.com:27017.
required: true
type: str
sharded_databases:
description:
- Enable sharding on the listed database.
- Can be supplied as a string or a list of strings.
- Sharding cannot be disabled on a database.
required: false
type: raw
mongos_process:
description:
- Provide a custom name for the mongos process you are connecting to.
- Most users can ignore this setting.
required: false
type: str
default: "mongos"
state:
description:
- Whether the shard should be present or absent from the Cluster.
required: false
type: str
default: present
choices:
- "absent"
- "present"
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+.
requirements: [ pymongo ]
'''
EXAMPLES = '''
- name: Add a replicaset shard named rs1 with a member running on port 27018 on mongodb0.example.net
community.mongodb.mongodb_shard:
login_user: admin
login_password: admin
shard: "rs1/mongodb0.example.net:27018"
state: present
- name: Add a standalone mongod shard running on port 27018 of mongodb0.example.net
community.mongodb.mongodb_shard:
login_user: admin
login_password: admin
shard: "mongodb0.example.net:27018"
state: present
- name: To remove a shard called 'rs1'
community.mongodb.mongodb_shard:
login_user: admin
login_password: admin
shard: rs1
state: absent
# Single node shard running on localhost
- name: Ensure shard rs0 exists
community.mongodb.mongodb_shard:
login_user: admin
login_password: secret
shard: "rs0/localhost:3001"
state: present
# Single node shard running on localhost
- name: Ensure shard rs1 exists
community.mongodb.mongodb_shard:
login_user: admin
login_password: secret
shard: "rs1/localhost:3002"
state: present
# Enable sharding on a few databases when creating the shard
- name: To remove a shard called 'rs1'
community.mongodb.mongodb_shard:
login_user: admin
login_password: admin
shard: rs1
sharded_databases:
- db1
- db2
state: present
'''
RETURN = '''
mongodb_shard:
description: The name of the shard to create.
returned: success
type: str
sharded_enabled:
description: Databases that have had sharding enabled during module execution.
returned: success when sharding is enabled
type: list
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
ssl_connection_options,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def shard_find(client, shard):
"""Check if a shard exists.
Args:
client (cursor): Mongodb cursor on admin database.
shard (str): shard to check.
Returns:
dict: when user exists, False otherwise.
"""
if '/' in shard:
s = shard.split('/')[0]
else:
s = shard
for shard in client["config"].shards.find({"_id": s}):
return shard
return False
def shard_add(client, shard):
try:
sh = client["admin"].command('addShard', shard)
except Exception as excep:
raise excep
return sh
def shard_remove(client, shard):
try:
sh = client["admin"].command('removeShard', shard)
except Exception as excep:
raise excep
return sh
def sharded_dbs(client):
'''
Returns the sharded databases
Args:
client (cursor): Mongodb cursor on admin database.
Returns:
a list of database names that are sharded
'''
sharded_databases = []
for entry in client["config"].databases.find({"partitioned": True}, {"_id": 1}):
sharded_databases.append(entry["_id"])
return sharded_databases
def enable_database_sharding(client, database):
'''
Enables sharding on a database
Args:
client (cursor): Mongodb cursor on admin database.
Returns:
true on success, false on failure
'''
s = False
db = client["admin"].command('enableSharding', database)
if db:
s = True
return s
def any_dbs_to_shard(client, sharded_databases):
'''
Return a list of databases that need to have sharding enabled
sharded_databases - Provided by module
cluster_sharded_databases - List of sharded dbs from the mongos
'''
dbs_to_shard = []
cluster_sharded_databases = sharded_dbs(client)
for db in sharded_databases:
if db not in cluster_sharded_databases:
dbs_to_shard.append(db)
return dbs_to_shard
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
mongos_process=dict(type='str', required=False, default="mongos"),
shard=dict(type='str', required=True),
sharded_databases=dict(type="raw", required=False),
state=dict(type='str', required=False, default='present', choices=['absent', 'present'])
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
login_host = module.params['login_host']
login_port = module.params['login_port']
shard = module.params['shard']
state = module.params['state']
sharded_databases = module.params['sharded_databases']
mongos_process = module.params['mongos_process']
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
if client["admin"].command("serverStatus")["process"] != mongos_process:
module.fail_json(msg="Process running on {0}:{1} is not a {2}".format(login_host, login_port, mongos_process))
shard_created = False
dbs_to_shard = []
if sharded_databases is not None:
if isinstance(sharded_databases, str):
sharded_databases = list(sharded_databases)
dbs_to_shard = any_dbs_to_shard(client, sharded_databases)
if module.check_mode:
if state == "present":
changed = False
if not shard_find(client, shard) or len(dbs_to_shard) > 0:
changed = True
elif state == "absent":
if not shard_find(client, shard):
changed = False
else:
changed = True
else:
if state == "present":
if not shard_find(client, shard):
shard_add(client, shard)
changed = True
else:
changed = False
if len(dbs_to_shard) > 0:
for db in dbs_to_shard:
enable_database_sharding(client, db)
changed = True
elif state == "absent":
if shard_find(client, shard):
shard_remove(client, shard)
changed = True
else:
changed = False
except Exception as e:
action = "add"
if state == "absent":
action = "remove"
module.fail_json(msg='Unable to {0} shard: %s'.format(action) % to_native(e), exception=traceback.format_exc())
result = {
"changed": changed,
"shard": shard,
}
if len(dbs_to_shard) > 0:
result['sharded_enabled'] = dbs_to_shard
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,216 @@
#!/usr/bin/python
# Copyright: (c) 2021, Rhys Campbell <rhyscampbell@blueiwn.ch>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_shard_tag
short_description: Manage Shard Tags.
description:
- Manage Shard Tags..
- Add and remove shard tags.
author: Rhys Campbell (@rhysmeister)
version_added: "1.3.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
name:
description:
- The name of the tag.
required: true
type: str
shard:
description:
- The name of the shard to assign or remove the tag from.
required: true
type: str
state:
description:
- The state of the zone.
required: false
type: str
choices:
- "present"
- "absent"
default: "present"
mongos_process:
description:
- Provide a custom name for the mongos process.
- Most users can ignore this setting.
required: false
type: str
default: "mongos"
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Add the NYC tag to a shard called rs0
community.mongodb.mongodb_shard_tag:
name: "NYC"
shard: "rs0"
state: "present"
- name: Remove the NYC tag from rs0
community.mongodb.mongodb_shard_tag:
name: "NYC"
shard: "rs0"
state: "absent"
'''
RETURN = r'''
changed:
description: True when a change has happened
returned: success
type: bool
msg:
description: A short description of what happened.
returned: failure
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def tag_exists(client, shard, tag):
'''
Returns True if the giventag is assign to the shard
@client - MongoDB connection
@tag - The zone to check for
@shard - The shard name
'''
status = None
result = client["config"].shards.find_one({"_id": shard, "tags": tag})
if result:
status = True
else:
status = False
return status
def add_zone_tag(client, shard, tag):
'''
Adds a tag to a shard
@client - MongoDB connection
@shard - The shard name
@tag - The tag or Zone name
'''
cmd_doc = OrderedDict([
('addShardToZone', shard),
('zone', tag),
])
client['admin'].command(cmd_doc)
def remove_zone_tag(client, shard, tag):
'''
Remove a zone tag from a shard.
@client - MongoDB connection
@shard - The shard name
@tag - The tag or Zone name
'''
cmd_doc = OrderedDict([
('removeShardFromZone', shard),
('zone', tag),
])
client['admin'].command(cmd_doc)
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
name=dict(type='str', required=True),
shard=dict(type='str', required=True),
mongos_process=dict(type='str', required=False, default="mongos"),
state=dict(type='str', default="present", choices=["present", "absent"]),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
state = module.params['state']
tag = module.params['name']
shard = module.params['shard']
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
if tag_exists(client, shard, tag):
if state == "present":
result['changed'] = False
result['msg'] = "The tag {0} is already assigned to the shard {1}".format(tag, shard)
elif state == "absent":
if not module.check_mode:
remove_zone_tag(client, shard, tag)
result['changed'] = True
result['msg'] = "The tag {0} was removed from the shard {1}".format(tag, shard)
else:
if state == "present":
if not module.check_mode:
add_zone_tag(client, shard, tag)
result['changed'] = True
result['msg'] = "The tag {0} was assigned to the shard {1}".format(tag, shard)
elif state == "absent":
result['changed'] = False
result['msg'] = "The tag {0} is not assigned to the shard {1}".format(tag, shard)
except Exception as excep:
module.fail_json(msg="An error occurred: {0}".format(excep))
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,323 @@
#!/usr/bin/python
# Copyright: (c) 2021, Rhys Campbell <rhyscampbell@blueiwn.ch>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_shard_zone
short_description: Manage Shard Zones.
description:
- Manage Shard Zones.
- Add and remove shard zones.
author: Rhys Campbell (@rhysmeister)
version_added: "1.3.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
name:
description:
- The name of the zone.
required: true
type: str
namespace:
description:
- The namespace the zone is assigned to
- Should be given in the form database.collection.
type: str
ranges:
description:
- The ranges assigned to the Zone.
type: list
elements: list
state:
description:
- The state of the zone.
required: false
type: str
choices:
- "present"
- "absent"
default: "present"
mongos_process:
description:
- Provide a custom name for the mongos process.
- Most users can ignore this setting.
required: false
type: str
default: "mongos"
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Add a shard zone for NYC
community.mongodb.mongodb_shard_zone:
name: "NYC"
namespace: "records.users"
ranges:
- [{ zipcode: "10001" }, { zipcode: "10281" }]
- [{ zipcode: "11201" }, { zipcode: "11240" }]
state: "present"
- name: Remove all zone ranges
community.mongodb.mongodb_shard_zone:
name: "NYC"
namespace: "records.users"
state: "absent"
- name: Remove a specific zone range
community.mongodb.mongodb_shard_zone:
name: "NYC"
namespace: "records.users"
ranges:
- [{ zipcode: "11201" }, { zipcode: "11240" }]
state: "absent"
'''
RETURN = r'''
changed:
description: True when a change has happened
returned: success
type: bool
msg:
description: A short description of what happened.
returned: failure
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
has_ordereddict = False
try:
from collections import OrderedDict
has_ordereddict = True
except ImportError as excep:
try:
from ordereddict import OrderedDict
has_ordereddict = True
except ImportError as excep:
pass
def zone_range_exists(client, namespace, min, max, tag):
'''
Returns true if a particular zone range exists
Record format seems to be different than the docs state in 4.4.6
{ "_id" : ObjectId("60e2e7cff7c9d447440bb114"),
"ns" : "records.users",
"min" : { "zipcode" : "10001" },
"max" : { "zipcode" : "10281" },
"tag" : "NYC" }
@client - MongoDB connection
@namespace - In the form database.collection
@min - The min range value
@max - The max range value
@tag - The tag or Zone name
'''
query = {
# "_id.ns": namespace, 4.4.X Bug??? ObjectId given as id
# "_id.min": min,
'ns': namespace,
'min': min,
'max': max,
'tag': tag
}
status = None
result = client["config"].tags.find_one(query)
if result:
status = True
else:
status = False
return status
def zone_exists(client, tag):
'''
Returns True if the given zone exists
@client - MongoDB connection
@tag - The zone to check for
'''
status = None
result = client["config"].shards.find_one({"tags": tag})
if result:
status = True
else:
status = False
return status
def add_zone_range(client, namespace, min, max, tag):
'''
Adds a zone range
@client - MongoDB connection
@namespace - In the form database.collection
@min - The min range value
@max - The max range value
@tag - The tag or Zone name
'''
cmd_doc = OrderedDict([
('updateZoneKeyRange', namespace),
('min', min),
('max', max),
('zone', tag),
])
client['admin'].command(cmd_doc)
def remove_zone_range(client, namespace, min, max):
'''
Remove a zone range.
We do this by setting the zone to None
@client - MongoDB connection
@namespace - In the form database.collection
@min - The min range value
@max - The max range value
'''
cmd_doc = OrderedDict([
('updateZoneKeyRange', namespace),
('min', min),
('max', max),
('zone', None),
])
client['admin'].command(cmd_doc)
def remove_all_zone_range_by_tag(client, tag):
result = client["config"].tags.find({"tag": tag})
for r in result:
remove_zone_range(client, r['ns'], r['min'], r['max'])
def zone_range_count(client, tag):
'''
Returns the count of records that exists for the given tag in config.tags
'''
return client['config'].tags.count_documents({"tag": tag})
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
name=dict(type='str', required=True),
namespace=dict(type='str'),
ranges=dict(type='list', elements='list'),
mongos_process=dict(type='str', required=False, default="mongos"),
state=dict(type='str', default="present", choices=["present", "absent"]),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
required_if=[("state", "present", ("namespace", "ranges"))]
)
if not has_ordereddict:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict')
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
state = module.params['state']
zone_name = module.params['name']
namespace = module.params['namespace']
ranges = module.params['ranges']
if ranges is not None:
if not isinstance(ranges, list) or not isinstance(ranges[0], list) or not isinstance(ranges[0][0], dict):
module.fail_json(msg="Provided ranges are invalid {0} {1} {2}".format(str(type(ranges)),
str(type(ranges[0])),
str(type(ranges[0][0]))))
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module)
client = mongo_auth(module, client)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
if not zone_exists(client, zone_name):
msg = ("The tag {0} does not exist. You need to associate a tag with"
" a shard before using this module. You can do that with the"
" mongodb_shard_tag module.".format(zone_name))
module.fail_json(msg=msg)
else:
# first check if the ranges exist
range_count = 0
if state == "present":
for range in ranges:
if zone_range_exists(client, namespace, range[0], range[1], zone_name):
range_count += 1
result['range_count'] = range_count
result['ranges'] = len(ranges)
if range_count == len(ranges): # All ranges are the same
result['changed'] = False
result['msg'] = "All Zone Ranges present for {0}".format(zone_name)
else:
for range in ranges:
if not module.check_mode:
add_zone_range(client, namespace, range[0], range[1], zone_name)
result['changed'] = True
result['msg'] = "Added zone ranges for {0}".format(zone_name)
elif state == "absent":
range_count = zone_range_count(client, zone_name)
deleted_count = 0
if range_count > 0 and ranges is None:
if not module.check_mode:
remove_all_zone_range_by_tag(client, zone_name)
deleted_count = range_count
result['changed'] = True
result['msg'] = "{0} zone ranges for {1} deleted.".format(deleted_count, zone_name)
elif ranges is not None:
for range in ranges:
if zone_range_exists(client, namespace, range[0], range[1], zone_name):
if not module.check_mode:
remove_zone_range(client, namespace, range[0], range[1])
deleted_count += 1
if deleted_count > 0:
result['changed'] = True
result['msg'] = "{0} zone ranges for {1} deleted.".format(deleted_count, zone_name)
else:
result['changed'] = False
result['msg'] = "The provided zone ranges are not present for {0}".format(zone_name)
else:
result['changed'] = False
result['msg'] = "No zone ranges present for {0}".format(zone_name)
except Exception as excep:
module.fail_json(msg="An error occurred: {0}".format(excep))
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,448 @@
#!/usr/bin/python
# 2020 Rhys Campbell <rhys.james.campbell@googlemail.com>
# https://github.com/rhysmeister
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
DOCUMENTATION = '''
---
module: mongodb_shell
author: Rhys Campbell (@rhysmeister)
version_added: "1.1.0"
short_description: Run commands via the MongoDB shell.
requirements:
- mongo or mongosh
description:
- Run commands via the MongoDB shell.
- Commands provided with the eval parameter or included in a Javascript file.
- Attempts to parse returned data into a format that Ansible can use.
- Module currently uses the mongo shell by default. This will change to mongosh in an upcoming version and support for mongo will be dropped
extends_documentation_fragment:
- community.mongodb.login_options
options:
mongo_cmd:
description:
- The MongoDB shell command.
type: str
default: "mongo"
db:
description:
- The database to run commands against
type: str
required: false
default: "test"
file:
description:
- Path to a file containing MongoDB commands.
type: str
eval:
description:
- A MongoDB command to run.
type: str
nodb:
description:
- Specify a non-default encoding for output.
type: bool
default: false
norc:
description:
- Prevents the shell from sourcing and evaluating ~/.mongorc.js on start up.
type: bool
default: false
quiet:
description:
- Silences output from the shell during the connection process..
type: bool
default: true
debug:
description:
- show additional debug info.
type: bool
default: false
transform:
description:
- Transform the output returned to the user.
- auto - Attempt to automatically decide the best tranformation.
- split - Split output on a character.
- json - parse as json.
- raw - Return the raw output.
type: str
choices:
- "auto"
- "split"
- "json"
- "raw"
default: "auto"
split_char:
description:
- Used by the split action in the transform stage.
type: str
default: " "
stringify:
description:
- Wraps the command in eval in JSON.stringify(<js cmd>) (mongo) or EJSON.stringify(<js cmd>) (mongosh).
- Useful for escaping documents that are returned in Extended JSON format.
- Automatically set to false when using mongo.
- Automatically set to true when using mongosh.
- Set explicitly to override automatic selection.
type: bool
default: null
additional_args:
description:
- Additional arguments to supply to the mongo command.
- Supply as key-value pairs.
- If the parameter is a valueless flag supply an empty string as the value.
type: raw
idempotent:
description:
- Provides a form of pseudo-idempotency to the module.
- We perform a hash calculation on the contents of the eval key or the file name provided in the file key.
- When the command is first execute a filed called <hash>.success will be created.
- The module will not rerun the command if this file exists and idempotent is set to true.
type: bool
default: false
omit:
description:
- Parameter to omit from the command line.
- This should match the parameter name that the MongoDB shell accepts not the module name.
type: list
elements: str
default: []
'''
EXAMPLES = '''
- name: Run the listDatabases command
community.mongodb.mongodb_shell:
login_user: user
login_password: secret
eval: "db.adminCommand('listDatabases')"
- name: List collections and stringify the output
community.mongodb.mongodb_shell:
login_user: user
login_password: secret
eval: "db.adminCommand('listCollections')"
stringify: yes
- name: Run the showBuiltinRoles command
community.mongodb.mongodb_shell:
login_user: user
login_password: secret
eval: "db.getRoles({showBuiltinRoles: true})"
- name: Run a js file containing MongoDB commands with pseudo-idempotency
community.mongodb.mongodb_shell:
login_user: user
login_password: secret
file: "/path/to/mongo/file.js"
idempotent: yes
- name: Provide a couple of additional cmd args
community.mongodb.mongodb_shell:
login_user: user
login_password: secret
eval: "db.adminCommand('listDatabases')"
additional_args:
verbose: True
networkMessageCompressors: "snappy"
'''
RETURN = '''
file:
description: JS file that was executed successfully.
returned: When a js file is used.
type: str
msg:
description: A message indicating what has happened.
returned: always
type: str
transformed_output:
description: Output from the mongo command. We attempt to parse this into a list or json where possible.
returned: on success
type: list
changed:
description: Change status.
returned: always
type: bool
failed:
description: Something went wrong.
returned: on failure
type: bool
out:
description: Raw stdout from mongo.
returned: when debug is set to true
type: str
err:
description: Raw stderr from mongo.
returned: when debug is set to true
type: str
rc:
description: Return code from mongo.
returned: when debug is set to true
type: int
'''
from ansible.module_utils.basic import AnsibleModule
import re
import json
import os
import shlex
import pipes
__metaclass__ = type
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
mongodb_common_argument_spec
)
def escape_param(param):
'''
Escapes the given parameter
@param - The parameter to escape
'''
escaped = None
if hasattr(shlex, 'quote'):
escaped = shlex.quote(param)
elif hasattr(pipes, 'quote'):
escaped = pipes.quote(param)
else:
escaped = "'" + param.replace("'", "'\\''") + "'"
return escaped
def add_arg_to_cmd(cmd_list, param_name, param_value, is_bool=False, omit=None):
"""
@cmd_list - List of cmd args.
@param_name - Param name / flag.
@param_value - Value of the parameter.
@is_bool - Flag is a boolean and has no value.
@omit - List of parameter to omit from the command line.
"""
if param_name.replace('-', '') not in omit:
if is_bool is False and param_value is not None:
cmd_list.append(param_name)
if param_name == "--eval":
cmd_list.append("{0}".format(escape_param(param_value)))
else:
cmd_list.append(param_value)
elif is_bool is True:
cmd_list.append(param_name)
return cmd_list
def extract_json_document(output):
"""
This is for specific type of mongo shell return data in the format SomeText()
https://github.com/ansible-collections/community.mongodb/issues/436
i.e.
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: state.hosts index: _id_ dup key: { _id: \"r1\" }"
}
})
"""
output = output.strip()
if re.match(r"^[a-zA-Z].*\(", output) and output.endswith(')'):
first_bracket = output.find('{')
last_bracket = output.rfind('}')
if first_bracket > 0 and last_bracket > 0:
tmp = output[first_bracket:last_bracket + 1]
# tmp = tmp.replace("\"", '\\\"')
tmp = tmp.replace('\n', '')
tmp = tmp.replace('\t', '')
if tmp is not None:
output = tmp
# elif re.match(r"^[a-zA-Z].*", output):
# first_bracket = output.find('{')
# last_bracket = output.rfind('}')
# tmp = output[first_bracket:last_bracket + 1]
# if tmp is not None:
# output = tmp
return output
def transform_output(output, transform_type, split_char):
output = extract_json_document(output)
if transform_type == "auto": # determine what transform_type to perform
if output.strip().startswith("{") or output.strip().startswith("["):
transform_type = "json"
elif isinstance(output.strip().split(None), list): # Splits on whitespace
transform_type = "split"
split_char = None
elif isinstance(output.strip().split(","), list):
transform_type = "split"
split_char = ","
elif isinstance(output.strip().split(" "), list):
transform_type = "split"
split_char = " "
elif isinstance(output.strip().split("|"), list):
transform_type = "split"
split_char = "|"
elif isinstance(output.strip().split("\t"), list):
transform_type = "split"
split_char = "\t"
else:
transform_type = "raw"
if transform_type == "json":
try:
output = json.loads(output)
except json.decoder.JSONDecodeError:
# Strip Extended JSON stuff like:
# "_id": ObjectId("58f56171ee9d4bd5e610d6b7"),
# "count": NumberLong(999),
output = re.sub(r'\:\s*\S+\s*\(\s*(\S+)\s*\)', r':\1', output)
try:
output = json.loads(output)
except json.decoder.JSONDecodeError as excep:
raise excep
elif transform_type == "split":
output = output.strip().split(split_char)
elif transform_type == "raw":
output = output.strip()
return output
def get_hash_value(module):
'''
Returns the hash value of either the provided file or eval command
'''
hash_value = None
try:
import hashlib
except ImportError as excep:
module.fail_json(msg="Unable to import hashlib: {0}".format(excep.message))
if module.params['file'] is not None:
hash_value = hashlib.md5(module.params['file'].encode('utf-8')).hexdigest()
else:
hash_value = hashlib.md5(module.params['eval'].encode('utf-8')).hexdigest()
return hash_value
def touch(fname, times=None):
with open(fname, 'a'):
os.utime(fname, times)
def main():
argument_spec = mongodb_common_argument_spec(ssl_options=False)
argument_spec.update(
mongo_cmd=dict(type='str', default="mongo"),
file=dict(type='str', required=False),
eval=dict(type='str', required=False),
db=dict(type='str', required=False, default="test"),
nodb=dict(type='bool', required=False, default=False),
norc=dict(type='bool', required=False, default=False),
quiet=dict(type='bool', required=False, default=True),
debug=dict(type='bool', required=False, default=False),
transform=dict(type='str', choices=["auto", "split", "json", "raw"], default="auto"),
split_char=dict(type='str', default=" "),
stringify=dict(type='bool', default=None),
additional_args=dict(type='raw'),
idempotent=dict(type='bool', default=False),
omit=dict(type='list', elements='str', default=[]),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['login_user', 'login_password']],
mutually_exclusive=[["eval", "file"]]
)
if module.params['mongo_cmd'] == "mongo" and module.params['stringify'] is None:
module.params['stringify'] = False
elif module.params['mongo_cmd'] == "mongosh" and module.params['stringify'] is None:
module.params['stringify'] = True
args = [
module.params['mongo_cmd'],
module.params['db']
]
hash_value = get_hash_value(module)
if module.params['idempotent']:
if os.path.isfile("{0}.success".format(hash_value)):
module.exit_json(changed=False,
msg="The file {0}.success was found meaning this "
"command has already successfully executed "
"on this MongoDB host.".format(hash_value))
if not module.params['file']:
if module.params['eval'].startswith("show "):
msg = "You cannot use any shell helper (e.g. use <dbname>, show dbs, etc.)"\
" inside the eval parameter because they are not valid JavaScript."
module.fail_json(msg=msg)
if module.params['stringify']:
if module.params['mongo_cmd'] != "mongosh":
module.params['eval'] = "JSON.stringify({0})".format(module.params['eval'])
else:
module.params['eval'] = "EJSON.stringify({0})".format(module.params['eval'])
omit = module.params['omit']
args = add_arg_to_cmd(args, "--host", module.params['login_host'], omit=omit)
args = add_arg_to_cmd(args, "--port", module.params['login_port'], omit=omit)
args = add_arg_to_cmd(args, "--username", module.params['login_user'], omit=omit)
args = add_arg_to_cmd(args, "--password", module.params['login_password'], omit=omit)
args = add_arg_to_cmd(args, "--authenticationDatabase", module.params['login_database'], omit=omit)
args = add_arg_to_cmd(args, "--eval", module.params['eval'], omit=omit)
args = add_arg_to_cmd(args, "--nodb", None, module.params['nodb'], omit=omit)
args = add_arg_to_cmd(args, "--norc", None, module.params['norc'], omit=omit)
args = add_arg_to_cmd(args, "--quiet", None, module.params['quiet'], omit=omit)
additional_args = module.params['additional_args']
if additional_args is not None:
for key, value in additional_args.items():
if isinstance(value, bool):
args.append(" --{0}".format(key))
elif isinstance(value, str) or isinstance(value, int):
args.append(" --{0} {1}".format(key, value))
if module.params['file']:
args.append(module.params['file'])
rc = None
out = ''
err = ''
result = {}
cmd = " ".join(str(item) for item in args)
(rc, out, err) = module.run_command(cmd, check_rc=False)
if module.params['debug']:
result['out'] = out
result['err'] = err
result['rc'] = rc
result['cmd'] = cmd
if rc != 0:
if err is None or err == "":
err = out
module.fail_json(msg=err.strip(), **result)
else:
result['changed'] = True
if module.params['idempotent']:
touch("{0}.success".format(hash_value))
try:
output = transform_output(out,
module.params['transform'],
module.params['split_char'])
result['transformed_output'] = output
result['msg'] = "transform type was {0}".format(module.params['transform'])
if module.params['file'] is not None:
result['file'] = module.params['file']
except Exception as excep:
result['msg'] = "Error tranforming output: {0}".format(str(excep))
result['transformed_output'] = None
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,138 @@
#!/usr/bin/python
# Copyright: (c) 2020, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_shutdown
short_description: Cleans up all database resources and then terminates the mongod/mongos process.
description:
- Cleans up all database resources and then terminates the process.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
force:
description:
- Specify true to force the mongod to shut down.
- Force shutdown interrupts any ongoing operations on the mongod and may result in unexpected behavior.
type: bool
default: false
timeout:
description:
- The number of seconds the primary should wait for a secondary to catch up.
type: int
default: 10
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Attempt to perform a clean shutdown
community.mongodb.mongodb_shutdown:
- name: Force shutdown with a timeout of 60 seconds
mongodb_maintenance:
force: true
timeout: 60
'''
RETURN = r'''
changed:
description: Whether the member was shutdown.
returned: success
type: bool
msg:
description: A short description of what happened.
returned: success
type: str
failed:
description: If something went wrong
returned: failed
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
ssl_connection_options,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
force=dict(type='bool', default=False),
timeout=dict(type='int', default=10)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
force = module.params['force']
timeout = module.params['timeout']
result = dict(
changed=False,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as excep:
module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
try:
cmd_doc = OrderedDict([
('shutdown', 1),
('force', force),
('timeoutSecs', timeout)
])
client['admin'].command(cmd_doc)
result["changed"] = True
result["msg"] = "mongod process was terminated sucessfully"
except Exception as excep:
if "connection closed" in str(excep):
result["changed"] = True
result["msg"] = "mongod process was terminated sucessfully"
else:
result["msg"] = "An error occurred: {0}".format(excep)
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,354 @@
#!/usr/bin/python
# Copyright: (c) 2018, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_status
short_description: Validates the status of the replicaset.
description:
- Validates the status of the replicaset.
- The module expects all replicaset nodes to be PRIMARY, SECONDARY or ARBITER.
- Will wait until a timeout for the replicaset state to converge if required.
- Can also be used to lookup the current PRIMARY member (see examples).
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replicaset name.
type: str
default: rs0
poll:
description:
- The maximum number of times to query for the replicaset status before the set converges or we fail.
type: int
default: 1
interval:
description:
- The number of seconds to wait between polling executions.
type: int
default: 30
validate:
description:
- The type of validate to perform on the replicaset.
- default, Suitable for most purposes. Validate that there are an odd
number of servers and one is PRIMARY and the remainder are in a SECONDARY
or ARBITER state.
- votes, Check the number of votes is odd and one is a PRIMARY and the
remainder are in a SECONDARY or ARBITER state. Authentication is
required here to get the replicaset configuration.
- minimal, Just checks that one server is in a PRIMARY state with the
remainder being SECONDARY or ARBITER.
type: str
choices:
- default
- votes
- minimal
default: default
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Check replicaset is healthy, fail if not after first attempt
community.mongodb.mongodb_status:
replica_set: rs0
when: ansible_hostname == "mongodb1"
- name: Wait for the replicaset rs0 to converge, check 5 times, 10 second interval between checks
community.mongodb.mongodb_status:
replica_set: rs0
poll: 5
interval: 10
when: ansible_hostname == "mongodb1"
# Get the replicaset status and then lookup the primary's hostname and save to a variable
- name: Ensure replicaset is stable before beginning
community.mongodb.mongodb_status:
login_user: "{{ admin_user }}"
login_password: "{{ admin_user_password }}"
poll: 3
interval: 10
register: rs
- name: Lookup PRIMARY replicaset member
set_fact:
primary: "{{ item.key.split('.')[0] }}"
loop: "{{ lookup('dict', rs.replicaset) }}"
when: "'PRIMARY' in item.value"
'''
RETURN = r'''
failed:
description: If the module has failed or not.
returned: always
type: bool
iterations:
description: Number of times the module has queried the replicaset status.
returned: always
type: int
msg:
description: Status message.
returned: always
type: str
replicaset:
description: The last queried status of all the members of the replicaset if obtainable.
returned: always
type: dict
'''
import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
ssl_connection_options,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def replicaset_config(client):
"""
Return the replicaset config document
https://docs.mongodb.com/manual/reference/command/replSetGetConfig/
"""
rs = client.admin.command('replSetGetConfig')
return rs
def replicaset_votes(config_document):
"""
Return the number of votes in the replicaset
"""
votes = 0
for member in config_document["config"]['members']:
votes += member['votes']
return votes
def replicaset_status(client, module):
"""
Return the replicaset status document from MongoDB
# https://docs.mongodb.com/manual/reference/command/replSetGetStatus/
"""
rs = client.admin.command('replSetGetStatus')
return rs
def replicaset_members(replicaset_document):
"""
Returns the members section of the MongoDB replicaset document
"""
return replicaset_document["members"]
def replicaset_friendly_document(members_document):
"""
Returns a version of the members document with
only the info this module requires: name & stateStr
"""
friendly_document = {}
for member in members_document:
friendly_document[member["name"]] = member["stateStr"]
return friendly_document
def replicaset_statuses(members_document, module):
"""
Return a list of the statuses
"""
statuses = []
for member in members_document:
statuses.append(members_document[member])
return statuses
def replicaset_good(statuses, module, votes):
"""
Returns true if the replicaset is in a "good" condition.
Good is defined as an odd number of servers >= 3, with
max one primary, and any even amount of
secondary and arbiter servers
"""
msg = "Unset"
status = None
valid_statuses = ["PRIMARY", "SECONDARY", "ARBITER"]
validate = module.params['validate']
if validate == "default":
if len(statuses) % 2 == 1:
if (statuses.count("PRIMARY") == 1
and ((statuses.count("SECONDARY")
+ statuses.count("ARBITER")) % 2 == 0)
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
msg = "Even number of servers in replicaset."
status = False
elif validate == "votes":
# Need to validate the number of votes in the replicaset
if votes % 2 == 1: # We have a good number of votes
if (statuses.count("PRIMARY") == 1
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
msg = "Even number of votes in replicaset."
status = False
elif validate == "minimal":
if (statuses.count("PRIMARY") == 1
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
module.fail_json(msg="Invalid value for validate has been provided: {0}".format(validate))
return status, msg
def replicaset_status_poll(client, module):
"""
client - MongoDB Client
poll - Number of times to poll
interval - interval between polling attempts
"""
iterations = 0 # How many times we have queried the cluster
failures = 0 # Number of failures when querying the replicaset
poll = module.params['poll']
interval = module.params['interval']
status = None
return_doc = {}
votes = None
config = None
while iterations < poll:
try:
iterations += 1
replicaset_document = replicaset_status(client, module)
members = replicaset_members(replicaset_document)
friendly_document = replicaset_friendly_document(members)
statuses = replicaset_statuses(friendly_document, module)
if module.params['validate'] == "votes": # Requires auth
config = replicaset_config(client)
votes = replicaset_votes(config)
status, msg = replicaset_good(statuses, module, votes)
if status: # replicaset looks good
return_doc = {"failures": failures,
"poll": poll,
"iterations": iterations,
"msg": msg,
"replicaset": friendly_document}
break
else:
failures += 1
return_doc = {"failures": failures,
"poll": poll,
"iterations": iterations,
"msg": msg,
"replicaset": friendly_document,
"failed": True}
if iterations == poll:
break
else:
time.sleep(interval)
except Exception as e:
failures += 1
return_doc['failed'] = True
return_doc['msg'] = str(e)
status = False
if iterations == poll:
break
else:
time.sleep(interval)
return_doc['failures'] = failures
return status, return_doc['msg'], return_doc
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
interval=dict(type='int', default=30),
poll=dict(type='int', default=1),
replica_set=dict(type='str', default="rs0"),
validate=dict(type='str', choices=['default', 'votes', 'minimal'], default='default'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
replica_set = module.params['replica_set']
msg = None
result = dict(
failed=False,
replica_set=replica_set,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
if len(replica_set) == 0:
module.fail_json(msg="Parameter 'replica_set' must not be an empty string")
try:
status, msg, return_doc = replicaset_status_poll(client, module) # Sort out the return doc
replicaset = return_doc['replicaset']
iterations = return_doc['iterations']
except Exception as e:
module.fail_json(msg='Unable to query replica_set info: {0}: {1}'.format(str(e), msg))
if status is False:
module.fail_json(msg=msg, replicaset=replicaset, iterations=iterations)
else:
module.exit_json(msg=msg, replicaset=replicaset, iterations=iterations)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,249 @@
#!/usr/bin/python
# Copyright: (c) 2020, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_stepdown
short_description: Step down the MongoDB node from a PRIMARY state.
description: >
Step down the MongoDB node from the PRIMARY state if it has that status.
Returns OK immediately if the member is already in the SECONDARY or ARBITER states.
Will wait until a timeout for the member state to reach SECONDARY or PRIMARY,
if the member state is currently STARTUP, RECOVERING, STARTUP2 or ROLLBACK,
before taking any needed action.
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
poll:
description:
- The maximum number of times query for the member status.
type: int
default: 1
interval:
description:
- The number of seconds to wait between poll executions.
type: int
default: 30
stepdown_seconds:
description:
- The number of seconds to step down the primary, during which time the stepdown member is ineligible for becoming primary.
type: int
default: 60
secondary_catch_up:
description:
- The secondaryCatchUpPeriodSecs parameter for the stepDown command.
- The number of seconds that mongod will wait for an electable secondary to catch up to the primary.
type: int
default: 10
force:
description:
- Optional. A boolean that determines whether the primary steps down if no electable and up-to-date secondary exists within the wait period.
type: bool
default: false
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Step down the current MongoDB member
community.mongodb.mongodb_stepdown:
login_user: admin
login_password: secret
- name: Step down the current MongoDB member, poll a maximum of 5 times if member state is recovering
community.mongodb.mongodb_stepdown:
login_user: admin
login_password: secret
poll: 5
interval: 10
'''
RETURN = r'''
failed:
description: If the module had failed or not.
returned: always
type: bool
iteration:
description: Number of times the module has queried the replicaset status.
returned: always
type: int
msg:
description: Status message.
returned: always
type: str
'''
import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
ssl_connection_options,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def member_status(client):
"""
Return the member status string
# https://docs.mongodb.com/manual/reference/command/replSetGetStatus/
"""
myStateStr = None
rs = client.admin.command('replSetGetStatus')
for member in rs["members"]:
if "self" in member.keys():
myStateStr = member["stateStr"]
return myStateStr
def member_stepdown(client, module):
"""
client - MongoDB Client
module - Ansible module object
"""
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
iterations = 0 # How many times we have queried the member
failures = 0 # Number of failures when querying the replicaset
poll = module.params['poll']
interval = module.params['interval']
stepdown_seconds = module.params['stepdown_seconds']
secondary_catch_up = module.params['secondary_catch_up']
force = module.params['force']
return_doc = {}
status = None
while iterations < poll:
try:
iterations += 1
return_doc['iterations'] = iterations
myStateStr = member_status(client)
if myStateStr == "PRIMARY":
# Run step down command
if module.check_mode:
return_doc["msg"] = "member was stepped down"
return_doc['changed'] = True
status = True
break
else:
cmd_doc = OrderedDict([
('replSetStepDown', stepdown_seconds),
('secondaryCatchUpPeriodSecs', secondary_catch_up),
('force', force)
])
try:
client.admin.command(cmd_doc) # For now we assume the stepDown was successful
except Exception as excep:
# 4.0 and below close the connection as part of the stepdown.
# This code should be removed once we support 4.2+ onwards
# https://tinyurl.com/yc79g9ay
if str(excep) == "connection closed":
pass
else:
raise excep
return_doc['changed'] = True
status = True
return_doc["msg"] = "member was stepped down"
break
elif myStateStr in ["SECONDARY", "ARBITER"]:
return_doc["msg"] = "member was already at {0} state".format(myStateStr)
return_doc['changed'] = False
status = True
break
elif myStateStr in ["STARTUP", "RECOVERING", "STARTUP2", "ROLLBACK"]:
time.sleep(interval) # Wait for interval
else:
return_doc["msg"] = "Unexpected member state {0}".format(myStateStr)
return_doc['changed'] = False
status = False
break
except Exception as e:
failures += 1
return_doc['failed'] = True
return_doc['changed'] = False
return_doc['msg'] = str(e)
status = False
if iterations == poll:
break
else:
time.sleep(interval)
return status, return_doc['msg'], return_doc
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
force=dict(type='bool', default=False),
interval=dict(type='int', default=30),
poll=dict(type='int', default=1),
secondary_catch_up=dict(type='int', default=10),
stepdown_seconds=dict(type='int', default=60)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
result = dict(
failed=False,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
try:
status, msg, return_doc = member_stepdown(client, module)
iterations = return_doc['iterations']
changed = return_doc['changed']
except Exception as e:
module.fail_json(msg='Unable to query replica_set info: %s' % str(e))
if status is False:
module.fail_json(msg=msg, iterations=iterations, changed=changed)
else:
module.exit_json(msg=msg, iterations=iterations, changed=changed)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,428 @@
#!/usr/bin/python
# (c) 2012, Elliott Foster <elliott@fourkitchens.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
# (c) 2014, Epic Games, Inc.
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: mongodb_user
short_description: Adds or removes a user from a MongoDB database
description:
- Adds or removes a user from a MongoDB database.
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replica set to connect to (automatically connects to primary for writes).
type: str
database:
description:
- The name of the database to add/remove the user from.
required: true
type: str
aliases: [db]
name:
description:
- The name of the user to add or remove.
required: true
aliases: [user]
type: str
password:
description:
- The password to use for the user.
type: str
aliases: [pass]
roles:
type: list
elements: raw
description:
- >
The database user roles valid values could either be one or more of the following strings:
'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase',
'dbAdminAnyDatabase'
- "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'."
- "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required."
state:
description:
- The database user state.
default: present
choices: [absent, present]
type: str
update_password:
default: always
choices: [always, on_create]
description:
- C(always) will always update passwords and cause the module to return changed.
- C(on_create) will only set the password for newly created users.
- This must be C(always) to use the localhost exception when adding the first admin user.
- This option is effectively ignored when using x.509 certs. It is defaulted to 'on_create' to maintain a \
a specific module behaviour when the login_database is '$external'.
type: str
create_for_localhost_exception:
type: path
description:
- This is parmeter is only useful for handling special treatment around the localhost exception.
- If C(login_user) is defined, then the localhost exception is not active and this parameter has no effect.
- If this file is NOT present (and C(login_user) is not defined), then touch this file after successfully adding the user.
- If this file is present (and C(login_user) is not defined), then skip this task.
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. Newer mongo server versions require newer
pymongo versions. @see http://api.mongodb.org/python/current/installation.html
requirements:
- "pymongo"
author:
- "Elliott Foster (@elliotttf)"
- "Julien Thebault (@Lujeni)"
'''
EXAMPLES = '''
- name: Create 'burgers' database user with name 'bob' and password '12345'.
community.mongodb.mongodb_user:
database: burgers
name: bob
password: 12345
state: present
- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly)
community.mongodb.mongodb_user:
database: burgers
name: bob
password: 12345
state: present
ssl: True
- name: Delete 'burgers' database user with name 'bob'.
community.mongodb.mongodb_user:
database: burgers
name: bob
state: absent
- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
community.mongodb.mongodb_user:
database: burgers
name: ben
password: 12345
roles: read
state: present
- name: Define roles
community.mongodb.mongodb_user:
database: burgers
name: jim
password: 12345
roles: readWrite,dbAdmin,userAdmin
state: present
- name: Define roles
community.mongodb.mongodb_user:
database: burgers
name: joe
password: 12345
roles: readWriteAnyDatabase
state: present
- name: Add a user to database in a replica set, the primary server is automatically discovered and written to
community.mongodb.mongodb_user:
database: burgers
name: bob
replica_set: belcher
password: 12345
roles: readWriteAnyDatabase
state: present
# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL).
# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials
# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin"
# This syntax requires mongodb 2.6+ and pymongo 2.5+
- name: Roles as a dictionary
community.mongodb.mongodb_user:
login_user: root
login_password: root_password
database: admin
user: oplog_reader
password: oplog_reader_password
state: present
replica_set: belcher
roles:
- db: local
role: read
- name: Adding a user with X.509 Member Authentication
community.mongodb.mongodb_user:
login_host: "mongodb-host.test"
login_port: 27001
login_database: "$external"
database: "admin"
name: "admin"
password: "test"
roles:
- dbAdminAnyDatabase
ssl: true
ssl_ca_certs: "/tmp/ca.crt"
ssl_certfile: "/tmp/tls.key" #cert and key in one file
state: present
auth_mechanism: "MONGODB-X509"
connection_options:
- "tlsAllowInvalidHostnames=true"
'''
RETURN = '''
user:
description: The name of the user to add or remove.
returned: success
type: str
'''
import os
import traceback
from operator import itemgetter
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six import binary_type, text_type
from ansible.module_utils._text import to_native, to_bytes
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def user_find(client, user, db_name):
"""Check if the user exists.
Args:
client (cursor): Mongodb cursor on admin database.
user (str): User to check.
db_name (str): User's database.
Returns:
dict: when user exists, False otherwise.
"""
try:
for mongo_user in client[db_name].command('usersInfo')['users']:
if mongo_user['user'] == user:
# NOTE: there is no 'db' field in mongo 2.4.
if 'db' not in mongo_user:
return mongo_user
# Workaround to make the condition works with AWS DocumentDB,
# since all users are in the admin database.
if mongo_user["db"] in [db_name, "admin"]:
return mongo_user
except Exception as excep:
if hasattr(excep, 'code') and excep.code == 11: # 11=UserNotFound
pass # Allow return False
else:
raise
return False
def user_add(module, client, db_name, user, password, roles):
# pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated
# without reproducing a lot of the logic in database.py of pymongo
db = client[db_name]
try:
exists = user_find(client, user, db_name)
except Exception as excep:
# We get this exception: "not authorized on admin to execute command"
# when auth is enabled on a new instance. The loalhost exception should
# allow us to create the first user. If the localhost exception does not apply,
# then user creation will also fail with unauthorized. So, ignore Unauthorized here.
if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized
exists = False
else:
raise
if exists:
user_add_db_command = 'updateUser'
else:
user_add_db_command = 'createUser'
user_dict = {}
if password is not None:
user_dict["pwd"] = password
if roles is not None:
user_dict["roles"] = roles
db.command(user_add_db_command, user, **user_dict)
def user_remove(module, client, db_name, user):
exists = user_find(client, user, db_name)
if exists:
if module.check_mode:
module.exit_json(changed=True, user=user)
db = client[db_name]
db.command("dropUser", user)
else:
module.exit_json(changed=False, user=user)
def check_if_roles_changed(uinfo, roles, db_name):
# We must be aware of users which can read the oplog on a replicaset
# Such users must have access to the local DB, but since this DB does not store users credentials
# and is not synchronized among replica sets, the user must be stored on the admin db
# Therefore their structure is the following :
# {
# "_id" : "admin.oplog_reader",
# "user" : "oplog_reader",
# "db" : "admin", # <-- admin DB
# "roles" : [
# {
# "role" : "read",
# "db" : "local" # <-- local DB
# }
# ]
# }
def make_sure_roles_are_a_list_of_dict(roles, db_name):
output = list()
for role in roles:
if isinstance(role, (binary_type, text_type)):
new_role = {"role": role, "db": db_name}
output.append(new_role)
else:
output.append(role)
return output
roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name)
uinfo_roles = uinfo.get('roles', [])
if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')):
return False
return True
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
database=dict(required=True, aliases=['db']),
name=dict(required=True, aliases=['user']),
password=dict(aliases=['pass'], no_log=True),
replica_set=dict(default=None),
roles=dict(default=None, type='list', elements='raw'),
state=dict(default='present', choices=['absent', 'present']),
update_password=dict(default="always", choices=["always", "on_create"], no_log=False),
create_for_localhost_exception=dict(default=None, type='path'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
login_user = module.params['login_user']
# Certs don't have a password but we want this module behaviour
if module.params['login_database'] == '$external':
module.params['update_password'] = 'on_create'
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
create_for_localhost_exception = module.params['create_for_localhost_exception']
b_create_for_localhost_exception = (
to_bytes(create_for_localhost_exception, errors='surrogate_or_strict')
if create_for_localhost_exception is not None else None
)
db_name = module.params['database']
user = module.params['name']
password = module.params['password']
roles = module.params['roles'] or []
state = module.params['state']
update_password = module.params['update_password']
try:
directConnection = False
if module.params['replica_set'] is None:
directConnection = True
client = get_mongodb_client(module, directConnection=directConnection)
client = mongo_auth(module, client, directConnection=directConnection)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
if state == 'present':
if password is None and update_password == 'always':
module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create')
if login_user is None and create_for_localhost_exception is not None:
if os.path.exists(b_create_for_localhost_exception):
try:
client.close()
except Exception:
pass
module.exit_json(changed=False, user=user, skipped=True, msg="The path in create_for_localhost_exception exists.")
try:
if update_password != 'always':
uinfo = user_find(client, user, db_name)
if uinfo:
password = None
if not check_if_roles_changed(uinfo, roles, db_name):
module.exit_json(changed=False, user=user)
if module.check_mode:
module.exit_json(changed=True, user=user)
user_add(module, client, db_name, user, password, roles)
except Exception as e:
module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc())
finally:
try:
client.close()
except Exception:
pass
# Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848
# newuinfo = user_find(client, user, db_name)
# if uinfo['role'] == newuinfo['role'] and CheckPasswordHere:
# module.exit_json(changed=False, user=user)
if login_user is None and create_for_localhost_exception is not None:
# localhost exception applied.
try:
# touch the file
open(b_create_for_localhost_exception, 'wb').close()
except Exception as e:
module.fail_json(
changed=True,
msg='Added user but unable to touch create_for_localhost_exception file %s: %s' % (create_for_localhost_exception, to_native(e)),
exception=traceback.format_exc()
)
elif state == 'absent':
try:
user_remove(module, client, db_name, user)
except Exception as e:
module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc())
finally:
try:
client.close()
except Exception:
pass
module.exit_json(changed=True, user=user)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,10 @@
ansible==2.9.20
ansible-lint==4.2.0
docker==4.1.0
flake8==3.7.9
molecule>=2.22,<3.0
pytest==4.6.9
python-vagrant==0.5.15
# sh 1.13.1 causes molecule yamllint to fail.
sh<1.13
pymongo==3.12.2

View File

@@ -0,0 +1,9 @@
ansible==2.9.20
ansible-lint==4.2.0
docker==4.1.0
flake8==3.7.9
molecule>=2.22,<3.0
pytest==5.3.4
python-vagrant==0.5.15
sh==1.13.1
pymongo==3.12.2

View File

@@ -0,0 +1,9 @@
ansible==2.9.20
ansible-lint==4.2.0
docker==4.1.0
flake8==3.7.9
molecule>=2.22,<3.0
pytest==5.3.4
python-vagrant==0.5.15
sh==1.13.1
pymongo==4.0.*

View File

@@ -0,0 +1,9 @@
ansible==2.9.20
ansible-lint==4.2.0
docker==4.1.0
flake8==3.7.9
molecule>=2.22,<3.0
pytest==5.3.4
python-vagrant==0.5.15
sh==1.13.1
pymongo==4.0.*

View File

@@ -0,0 +1,33 @@
---
# Based on ansible-lint config
extends: default
rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
colons:
max-spaces-after: -1
level: error
commas:
max-spaces-after: -1
level: error
comments: disable
comments-indentation: disable
document-start: disable
empty-lines:
max: 3
level: error
hyphens:
level: error
indentation: disable
key-duplicates: enable
line-length: disable
new-line-at-end-of-file: disable
new-lines:
type: unix
trailing-spaces: disable
truthy: disable

View File

@@ -0,0 +1,84 @@
mongodb_auth
============
This role to enables auth on MongoDB servers, adds the first admin user, and adds a list of other users.
If your mongo instance requires ssl or an alternative auth_mechanism, please use
[`module_defaults`](https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html)
to provide the default auth details for `community.mongodb.mongodb_user` (these defaults are ignored
when adding the initial admin user with the localhost exception).
If running this on a MongoDB server that already has an admin user (ie when using this role to audit
an alternate install method), you must touch `/root/mongodb_admin.success` or you will get an error
when this role tries to add the admin user again.
Role Variables
--------------
* `mongod_host`: The domain or ip to use to communicate with mongod. Default localhost.
* `mongod_port`: The port used by the mongod process. Default 27017.
* `mongod_package`: The mongod package to install. Default mongodb-org-server.
* `authorization`: Enable authorization. Default enabled.
* `mongodb_admin_db`: MongoDB admin database (for adding users). Default admin.
* `mongodb_admin_user`: MongoDB admin username. Default admin.
* `mongodb_admin_pwd`: MongoDB admin password. Defaults to value of mongodb_admin_default_pwd.
* `mongodb_admin_default_pwd`: MongoDB admin password (for parent roles to override without overriding user's password). Default admin.
* `mongodb_users`: List of additional users to add. Each user dict should include fields: db, user, pwd, roles
* `mongodb_force_update_password`: Whether or not to force a password update for any users in mongodb_users. Setting this to yes will result in 'changed' on every run, even if the password is the same. Setting this to no only adds a password when creating the user.
IMPORTANT NOTE: It is expected that mongodb_admin_user & mongodb_admin_pwd values be overridden in your own file protected by Ansible Vault. Any production environments should protect these values. For more information see [Ansible Vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
Dependencies
------------
mongodb_repository
Example Playbook
----------------
Install MongoDB preparing hosts for a Replicaset
```yaml
- hosts: servers
roles:
- { role: "community.mongodb.mongodb_repository" }
- { role: "community.mongodb.mongodb_mongod" }
tasks:
- name: Initialise MongoDB Replicaset rs0
community.mongodb.mongodb_replicaset:
login_database: "admin"
login_host: localhost
replica_set: "rs0"
members:
- "mongodb1"
- "mongodb2"
- "mongodb3"
when: ansible_hostname == "mongodb1"
register: repl
- name: Ensure replicaset has reached a converged state
community.mongodb.mongodb_status:
replica_set: "rs0"
poll: 10
interval: 10
when: repl.changed == True
- name: Import mongodb_auth role
include_role:
name: mongodb_auth
vars:
mongod_host: "127.0.0.1"
mongodb_admin_pwd: "f00b@r"
when: ansible_hostname == "mongodb1"
```
License
-------
BSD
Author Information
------------------
Jacob Floyd (https://github.com/cognifloyd)

View File

@@ -0,0 +1,27 @@
---
# defaults file for mongodb_auth
mongod_port: 27017
mongod_package: "mongodb-org-server"
authorization: "enabled"
# when adding auth, the login credentials to use
mongodb_admin_user: admin
# For production use - please change the admin password!
mongodb_admin_pwd: "{{ mongodb_default_admin_pwd }}"
# The default is separate so other roles can provide a default without overriding a user provided password.
mongodb_default_admin_pwd: admin
# allow for alternate admin roles (eg userAdminAnyDatabase)
mongodb_admin_roles: "root"
# Additional users to add.
mongodb_users: []
# - db: somedatabase
# user: someuser
# pwd: "S0meP@ss"
# roles: readWrite
# whether or not to force a password update for any users in mongodb_users
# Setting this to yes will result in 'changed' on every run, even if the password is the same.
# See the comment in tasks/main.yml for more details.
mongodb_force_update_password: no

View File

@@ -0,0 +1,30 @@
---
galaxy_info:
author: Jacob Floyd
description: Configure auth on MongoDB servers.
license: GPLv3
min_ansible_version: 2.9
# platforms:
# - name: Fedora
# versions:
# - all
# - 25
# - name: SomePlatform
# versions:
# - all
# - 1.0
# - 7
# - 99.99
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []

View File

@@ -0,0 +1,23 @@
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
{% if item.env is defined %}
{% for var, value in item.env.items() %}
{% if value %}
ENV {{ var }} {{ value }}
{% endif %}
{% endfor %}
{% endif %}
# Add systemd-sysv package for Debian to get systemd working (and procps for sysctl) and netbase for firewalld
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates iproute2 systemd-sysv procps netbase && apt-get clean; \
elif [ $(command -v dnf) ] && grep -q 'platform:el8' /etc/os-release ; then dnf makecache && dnf update -y && dnf --assumeyes install python3 sudo python3-devel bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && rm -Rf /usr/share/doc && rm -Rf /usr/share/man && dnf clean all && cp /bin/true /sbin/agetty; \
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash iproute && dnf clean all; \
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml iproute2 && zypper clean -a; \
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates iproute2 && xbps-remove -O; fi

View File

@@ -0,0 +1,44 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-data:
line-length: disable
platforms:
- name: centos_7
image: centos:7
command: /sbin/init
privileged: True
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- name: ubuntu_16
image: ubuntu:16.04
command: /sbin/init
privileged: True
- name: ubuntu_18
image: ubuntu:18.04
command: /sbin/init
privileged: True
- name: debian_buster
image: debian:buster
command: /sbin/init
privileged: True
- name: debian_stretch
image: debian:stretch
command: /sbin/init
privileged: True
provisioner:
name: ansible
lint:
name: ansible-lint
enabled: false
verifier:
name: testinfra
lint:
name: flake8
options:
ignore: 'E501'

View File

@@ -0,0 +1,65 @@
---
- name: Converge
hosts: all
become: yes
vars:
# for this test, each machine is an isolated mongod instance
replicaset: false
sharding: false
# initially disable authorization on some hosts
hosts_with_auth_disabled:
- ubuntu_18
- debian_buster
# add some users for some of the hosts
hosts_with_extra_user:
- centos_8
- ubuntu_16
- debian_buster
mongodb_users_empty: []
mongodb_users_full:
- db: somedatabase
user: someuser
pwd: "S0meP@ss"
roles: readWrite
- db: somedatabase
user: otheruser
pwd: "0th3rP@ss"
roles: readWrite
mongodb_users: "{% if inventory_hostname in hosts_with_extra_user %}{{ mongodb_users_full }}{% else %}{{ mongodb_users_empty }}{% endif %}"
roles:
- role: mongodb_repository
tags: molecule-idempotence-notest
- role: mongodb_mongod
authorization: "{% if inventory_hostname in hosts_with_auth_disabled %}disabled{% else %}enabled{% endif %}"
tags: molecule-idempotence-notest # avoids false positive where replacing conf file disables authorization again
tasks:
- name: Add EPEL repo to CentOS 7 to allow installing pip package
become: yes
yum:
name: epel-release
state: present
when: inventory_hostname == 'centos_7'
- name: Install python stuff
package:
name:
- "python{% if needs_3 %}3{% endif %}-setuptools"
- "python{% if needs_3 %}3{% endif %}-pip"
vars:
needs_3: "{{ ansible_facts.python.version.major == 3 }}"
- name: Install pymongo
pip:
name: "{{ 'pymongo==3.12.*' if ansible_python_version[0:3] | float < 3.6 else 'pymongo' }}"
- name: Enable mongo auth
include_role:
name: mongodb_auth
- name: Install MongoDB Shell
package:
name: mongodb-org-shell

View File

@@ -0,0 +1,56 @@
import os
import yaml
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ["MOLECULE_INVENTORY_FILE"]
).get_hosts("all")
def include_vars(host):
ansible = host.ansible("include_vars",
'file="../../defaults/main.yml"',
False,
False)
return ansible
def test_mongod_cnf_file(host):
f = host.file("/etc/mongod.conf")
assert f.exists
assert yaml.safe_load(f.content)["security"]["authorization"] == "enabled"
def test_mongod_service(host):
mongod_service = include_vars(host)["ansible_facts"].get("mongod_service", "mongod")
s = host.service(mongod_service)
assert s.is_running
assert s.is_enabled
def test_mongod_port(host):
port = include_vars(host)["ansible_facts"].get("mongod_port", 27017)
s = host.socket("tcp://0.0.0.0:{0}".format(port))
assert s.is_listening
def test_mongo_shell_connectivity(host):
"""
Tests that we can connect to mongos via the shell annd run a cmd
"""
facts = include_vars(host)["ansible_facts"]
port = facts.get("mongod_port", 27017)
user = facts.get("mongod_admin_user", "admin")
pwd = facts.get("mongod_default_admin_pwd", "admin")
cmd = host.run(
"mongo admin --username {user} --password {pwd} --port {port} --eval 'db.runCommand({{listDatabases: 1}})'".format(
user=user, pwd=pwd, port=port
)
)
assert cmd.rc == 0
assert "admin" in cmd.stdout

View File

@@ -0,0 +1,54 @@
---
dependency:
name: galaxy
driver:
name: vagrant
provider:
name: virtualbox
lint:
name: yamllint
options:
config-data:
line-length: disable
platforms:
- name: centos-7
box: centos/7
interfaces:
- network_name: private_network
type: dhcp
auto_config: true
- name: ubuntu-16
box: ubuntu/xenial64
interfaces:
- network_name: private_network
type: dhcp
auto_config: true
- name: ubuntu-18
box: ubuntu/bionic64
interfaces:
- network_name: private_network
type: dhcp
auto_config: true
- name: debian-buster
box: debian/buster64
interfaces:
- network_name: private_network
type: dhcp
auto_config: true
- name: debian-stretch
box: debian/contrib-stretch64 # Standard debian/stretch64 had issues: Unable to locate package linux-headers-4.9.0-9-amd64
interfaces:
- network_name: private_network
type: dhcp
auto_config: true
provisioner:
name: ansible
lint:
name: ansible-lint
enabled: false
verifier:
name: testinfra
lint:
name: flake8
options:
ignore: 'E501'

View File

@@ -0,0 +1,66 @@
---
- name: Converge
hosts: all
become: yes
vars:
# for this test, each machine is an isolated mongod instance
replicaset: false
sharding: false
# initially disable authorization on some hosts
hosts_with_auth_disabled:
- ubuntu_18
- debian_buster
# add some users for some of the hosts
hosts_with_extra_user:
- centos_8
- ubuntu_16
- debian_buster
mongodb_users_empty: []
mongodb_users_full:
- db: somedatabase
user: someuser
pwd: "S0meP@ss"
roles: readWrite
- db: somedatabase
user: otheruser
pwd: "0th3rP@ss"
roles: readWrite
mongodb_users: "{% if inventory_hostname in hosts_with_extra_user %}{{ mongodb_users_full }}{% else %}{{ mongodb_users_empty }}{% endif %}"
roles:
- role: mongodb_repository
tags: molecule-idempotence-notest
- role: mongodb_mongod
vars:
authorization: "{% if inventory_hostname in hosts_with_auth_disabled %}disabled{% else %}enabled{% endif %}"
tags: molecule-idempotence-notest # avoids false positive where replacing conf file disables authorization again
tasks:
- name: Add EPEL repo to CentOS 7 to allow installing pip package
become: yes
yum:
name: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_facts.distribution_major_version }}.noarch.rpm"
state: present
when: inventory_hostname == 'centos_7'
- name: Install python stuff
package:
name:
- "python{% if needs_3 %}3{% endif %}-setuptools"
- "python{% if needs_3 %}3{% endif %}-pip"
vars:
needs_3: "{{ ansible_facts.python.version.major == 3 }}"
- name: Install pymongo
pip:
name: "{{ 'pymongo==3.12.*' if ansible_python_version[0:3] | float < 3.6 else 'pymongo' }}"
- name: Enable mongo auth
include_role:
name: mongodb_auth
- name: Install MongoDB Shell
package:
name: mongodb-org-shell

View File

@@ -0,0 +1,40 @@
---
- name: Prepare
hosts: all
become: yes
vars:
avahi_packages_redhat:
- "avahi"
- "nss-mdns"
avahi_packages_debian:
- "avahi-daemon"
- "avahi-discover"
- "libnss-mdns"
tasks:
- name: Ensure epel is available
yum:
name: epel-release
state: present
when: ansible_os_family == "RedHat"
- name: Install avahi packages
package:
name: "{{ avahi_packages_redhat }}"
state: present
when: ansible_os_family == "RedHat"
- name: Install avahi packages
package:
name: "{{ avahi_packages_debian }}"
state: present
when: ansible_os_family == "Debian"
# debian-stretch seems to require a reboot for avahi-daemon to run
- name: Reboot host
reboot:
- name: Ensure avahi-daemon is started
service:
name: avahi-daemon
state: started

View File

@@ -0,0 +1,56 @@
import os
import yaml
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ["MOLECULE_INVENTORY_FILE"]
).get_hosts("all")
def include_vars(host):
ansible = host.ansible("include_vars",
'file="../../defaults/main.yml"',
False,
False)
return ansible
def test_mongod_cnf_file(host):
f = host.file("/etc/mongod.conf")
assert f.exists
assert yaml.safe_load(f.content)["security"]["authorization"] == "enabled"
def test_mongod_service(host):
mongod_service = include_vars(host)["ansible_facts"].get("mongod_service", "mongod")
s = host.service(mongod_service)
assert s.is_running
assert s.is_enabled
def test_mongod_port(host):
port = include_vars(host)["ansible_facts"].get("mongod_port", 27017)
s = host.socket("tcp://0.0.0.0:{0}".format(port))
assert s.is_listening
def test_mongo_shell_connectivity(host):
"""
Tests that we can connect to mongos via the shell annd run a cmd
"""
facts = include_vars(host)["ansible_facts"]
port = facts.get("mongod_port", 27017)
user = facts.get("mongod_admin_user", "admin")
pwd = facts.get("mongod_default_admin_pwd", "admin")
cmd = host.run(
"mongo admin --username {user} --password {pwd} --port {port} --eval 'db.runCommand({{listDatabases: 1}})'".format(
user=user, pwd=pwd, port=port
)
)
assert cmd.rc == 0
assert "admin" in cmd.stdout

View File

@@ -0,0 +1,117 @@
---
# tasks file for mongodb_auth
- name: Include OS-specific vars
include_vars:
file: "{{ lookup('first_found', params) }}"
vars:
params:
paths:
- "vars"
files:
- "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_version }}.yml"
- "{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml"
- "{{ ansible_facts.distribution }}.yml"
- "{{ ansible_facts.os_family }}.yml"
- default.yml
tags:
- "vars"
- name: Ensure mongod and pyyaml packages are installed
package:
name:
- "{{ mongod_package }}"
# pyyaml is used to validate yaml files on change
- "{{ pyyaml_package }}"
register: _pkg
until: _pkg is succeeded
retries: 5
tags:
- "pkg"
- name: Warn about default credentials
when: mongodb_admin_pwd == mongodb_default_admin_pwd
debug:
msg: "[WARNING] Using default admin credentials for mongodb admin account! Please change them!"
tags:
- "debug"
- name: Add mongo admin user with localhost exception
community.mongodb.mongodb_user:
state: present
# on_create triggers additional queries that are not compatible with localhost exception
update_password: always
name: "{{ mongodb_admin_user }}"
password: "{{ mongodb_admin_pwd }}"
database: admin
roles: "{{ mongodb_admin_roles }}"
login_host: localhost
login_port: "{{ mongod_port | string }}" # silence implicit int->str conversion warning
create_for_localhost_exception: /root/mongodb_admin.success
module_defaults:
community.mongodb.mongodb_user: {}
tags:
- "mongodb"
- "setup"
- "admin_user"
- name: Enable security section in mongod.conf
lineinfile:
path: /etc/mongod.conf
regexp: |-
^[#'"\s]*security['"]?\s*:
line: 'security:'
validate: |
{{ mongodb_python }} -c '
import yaml, io
if "security" not in yaml.safe_load(io.open("%s")):
exit(1)
'
tags:
- "mongodb"
- "setup"
- name: Enable authentication in mongod.conf
lineinfile:
path: /etc/mongod.conf
insertafter: '^security:'
# two space indentation (the default) assumed
line: ' authorization: {{ authorization }}'
regexp: |-
^[#'"\s]+authorization['"]?\s*:
validate: |
{{ mongodb_python }} -c '
import yaml, io
if yaml.safe_load(io.open("%s"))["security"]["authorization"] != "{{ authorization }}":
exit(1)
'
register: _enable_mongo_auth
tags:
- "mongodb"
- "setup"
# This is a task instead of a handler so we can add users right away
- name: Restart mongodb to enable auth before adding additional users
# This allows us to safely assume auth is already enabled when adding more users
when: _enable_mongo_auth is changed
service:
name: mongod
state: restarted
tags:
- "mongodb"
- "service"
- "setup"
- name: Add additional mongo users
include_tasks: mongodb_auth_user.yml
loop: "{{ mongodb_users }}"
loop_control:
loop_var: _mongodb_user
# using loop_control: label does not obscure the password in output for verbosity > 1
# So, loop over an include where the task name will include the username + db, but the loop var won't print out.
no_log: yes
tags:
- "mongodb"
- "app_user"

View File

@@ -0,0 +1,32 @@
---
- name: "Add mongo auth user - {{ _mongodb_user.user }} on {{ _mongodb_user.db }}"
community.mongodb.mongodb_user:
state: present
# NOTE: on_create is idempotent, always is not.
# With `update_password: on_create`, mongodb_user checks to see if the user
# (a) exists on the db, and (b) has the same roles,
# and then it only adds the user if it's not there or the roles have changed.
# With `update_password: always`, mongodb_user cannot tell if the password
# needs to be changed without attempting a login with those credentials.
# But mongodb_user does not currently implement such a check.
# A comment in mongodb_user points to https://jira.mongodb.org/browse/SERVER-22848
update_password: "{{ mongodb_force_update_password|ternary('always', 'on_create') }}"
name: "{{ _mongodb_user.user }}"
password: "{{ _mongodb_user.pwd }}"
database: "{{ _mongodb_user.db }}"
roles: "{{ _mongodb_user.roles|default('readWrite') }}"
login_host: localhost
login_port: "{{ mongod_port | string }}" # silence implicit int->str conversion warning
login_user: "{{ mongodb_admin_user }}"
login_password: "{{ mongodb_admin_pwd }}"
login_database: admin
# to provide additional auth details (eg for ssl* or auth_mechanism, set module_defaults in playbook)
# module_defaults:
# community.mongodb.mongodb_user:
# auth_mechanism: ...
tags:
- "mongodb"
- "app_user"

View File

@@ -0,0 +1,2 @@
---
pyyaml_package: PyYAML

View File

@@ -0,0 +1,2 @@
---
pyyaml_package: python3-pyyaml

View File

@@ -0,0 +1,2 @@
---
pyyaml_package: "python{% if ansible_facts.python.version.major == 3 %}3{% endif %}-yaml"

View File

@@ -0,0 +1,2 @@
---
mongodb_python: "{{ ansible_python_interpreter | default( (ansible_python|default({})).get('executable', 'python') ) }}"

View File

@@ -0,0 +1,33 @@
---
# Based on ansible-lint config
extends: default
rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
colons:
max-spaces-after: -1
level: error
commas:
max-spaces-after: -1
level: error
comments: disable
comments-indentation: disable
document-start: disable
empty-lines:
max: 3
level: error
hyphens:
level: error
indentation: disable
key-duplicates: enable
line-length: disable
new-line-at-end-of-file: disable
new-lines:
type: unix
trailing-spaces: disable
truthy: disable

View File

@@ -0,0 +1,58 @@
mongodb_config
==============
A simple role to aid in setting up a CSRS Config Server Replicaset for a MongoDB sharded cluster.
Role Variables
--------------
* `config_port`: The port used by the mongos process. Default 27019.
* `mongod_service`: The name of the mongod service. Default mongod.
* `mongodb_user`: The Linux OS user for MongoDB. Default mongod.
* `mongodb_group`: The Linux OS user group for MongoDB. Default mongod.
* `pid_file`: The pid file for mongos. Default /run/mongodb/mongos.pid.
* `log_path`: Path of the log file. Default /var/log/mongodb/mongod.log.
* `bind_ip`: The IP address mongod will bind to. Default 0.0.0.0.
* `bind_ip_all`: Have mongod bind to all IP addresses instead of specifying `bind_ip`. Default false.
* `config_repl_set_name`: The replicaset name for the config servers. Default cfg.
* `authorization`: Enable authorization. Default enabled.
* `openssl_keyfile_content`: The kexfile content that MongoDB uses to authenticate within a replicaset. Generate with cmd: openssl rand -base64 756.
* `openssl_keyfile_path`: Put the openssl_keyfile at this path. Default: /etc/keyfile
* `mongod_package`: The name of the mongod installation package. Default mongodb-org-server.
replicaset: When enabled add a replication section to the configuration. Default true.
* `net_compressors`: If this is set, this sets `net.compression.compressors` in mongod.conf.
* `mongod_config_template`: If defined allows to override path to mongod config template with custom configuration. Default "mongod.conf.j2"
* `skip_restart`: If set to `true` will skip restarting mongod service when config file or the keyfile content changes. Default `true`.
* `db_path`: Path to database data location. Default `/var/lib/mongodb` on Debian based distributions, `/var/lib/mongo` for others.
* `mongodb_use_tls`: Wether to use tls. Default false.
* `mongodb_certificate_key_file`: Path to the PEM-file containing the certficate and private key.
* `mongodb_certificate_ca_file`: Path to the CA-file.
Dependencies
------------
mongodb_repository
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables
passed in as parameters) is always nice for users too:
```yaml
- hosts: servers
roles:
- { role: mongodb_repository }
- { role: mongodb_config, config_repl_set_name: "mycustomrs" }
```
License
-------
BSD
Author Information
------------------
Rhys Campbell (https://github.com/rhysmeister)

View File

@@ -0,0 +1,34 @@
---
# defaults file for mongodb_config
pid_file: /var/run/mongodb/mongod.pid
bind_ip: 0.0.0.0
bind_ip_all: false
log_path: /var/log/mongodb/mongod.log
# config_port is in vars to facilitate molecule tests
config_repl_set_name: cfg
authorization: enabled
openssl_keyfile_path: /etc/keyfile
openssl_keyfile_content: |
Z2CeA9BMcoY5AUWoegjv/XWL2MA1SQcL4HvmRjYaTjSp/xosJy+LL2X3OQb1xVWC
rO2e6Tu6A3R4muunitI6Vr0IKeU5UbTpR0N4hSU6HDrV9z2PIEWlkQqKh01ZRLEY
V3hR73acj0jA8eWIWeiV039d18jvMb8X2h8409lfcD6PPJJGjyaC8S4LY/TrsK2z
tx+l/vqOOAMhGB5mEMjx1LXUMsRG9ot6vFu9I5LPd1A4q9xw9jddYK5C6YTLccun
ZyCDsv7ImkCprV0+0vhTyxIEnfaNtvOlWypuvmRr/DEyd2NPowd1n6C+rgk8gs1t
SGLCZP93gXza0rIoQzHtuf5pOJK9qyKjuNtuuLa/KFsida8a69JXn7fmS0IIja0m
Ir0OrQ2Ta3n4VbQwQo97BWODWmkgzz0mUd6VmMps5zLsCW1vVqYFQHuAAbLekW0q
8JRm8OQ6n2hp8j4zYd3/Qw7vqsVj8sHicNB0bCW29b64H4f2J/AcUA/cm0xSUQyb
+myeCB4vWvydh5AfFVnw7sXvzU6egaYRomdmrl59QrTDneJu13hwzIchsFparoWJ
XjpldopGeDaJLU18ga7MSL02ozB+EoJ14DJxQU7E5MQk7fDMPeitXKZ8ymxb7LeA
k0Rtc/JQM8aDLoRklhLZRRARBrv1RLo8DM8CB2q4s+FwVU4QJl7mFyiwk3eTN6sN
PTgFRo3/dHsEA2OwGG+hnGFGnoYf2mkECR5jqai83CXgva9v2rPNjDTJYHpmd3I0
fNijueXZZdzUA58y8mcoSGVYdRhr0g8jaWQ12PZEgX5Nnlekh5GHG0j8HT4qj/0Y
D3xVuE3WvrhldY5EOsaTt2ZXZx5REmJDIW1KcnvQKiVDJ2QzP5xdXYA0hh3TdTVE
sb4UreMw/WyBpANiICMlJRBgSd0f0VGMlYzLX2BL14YpNnLhmoQqKzfBN6v2XAEG
mJfrCUVuP1nBEklk23lYkNi/ohe+aodNjdN+2DHp42sGZHYP
mongod_package: "mongodb-org-server"
replicaset: true
net_compressors: null
mongod_config_template: "configsrv.conf.j2"
skip_restart: true
db_path: "{{ '/var/lib/mongodb' if ansible_os_family == 'Debian' else '/var/lib/mongo' if ansible_os_family == 'RedHat' else '/var/lib/mongo' }}"
mongodb_use_tls: false

View File

@@ -0,0 +1,14 @@
---
- name: Restart mongod
listen: Restart mongod service
service:
name: "{{ mongod_service }}"
state: restarted
when: not skip_restart
- name: Wait for port to become active
listen: Restart mongod service
wait_for:
host: "{{ bind_ip }}"
port: "{{ config_port }}"
when: not skip_restart

View File

@@ -0,0 +1,57 @@
---
galaxy_info:
author: Rhys Campbell
description: Configure the CSRS Config Server Replicaset for a MongoDB sharded cluster. (Use mongodb_mongod for Standalone installations - this does not create mongo.conf)
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Some suggested licenses:
# - BSD (default)
# - MIT
# - GPLv2
# - GPLv3
# - Apache
# - CC-BY
license: GPLv3
min_ansible_version: 1.2
# If this a Container Enabled role, provide the minimum Ansible Container version.
# min_ansible_container_version:
# Optionally specify the branch Galaxy will use when accessing the GitHub
# repo for this role. During role install, if no tags are available,
# Galaxy will use this branch. During import Galaxy will access files on
# this branch. If Travis integration is configured, only notifications for this
# branch will be accepted. Otherwise, in all cases, the repo's default branch
# (usually master) will be used.
# github_branch:
#
# platforms is a list of platforms, and each platform has a name and a list of versions.
#
# platforms:
# - name: Fedora
# versions:
# - all
# - 25
# - name: SomePlatform
# versions:
# - all
# - 1.0
# - 7
# - 99.99
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
# if you add dependencies to this list.

View File

@@ -0,0 +1,23 @@
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
{% if item.env is defined %}
{% for var, value in item.env.items() %}
{% if value %}
ENV {{ var }} {{ value }}
{% endif %}
{% endfor %}
{% endif %}
# Add systemd-sysv package for Debian to get systemd working (and procps for sysctl) and netbase for firewalld
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates iproute2 systemd-sysv procps netbase && apt-get clean; \
elif [ $(command -v dnf) ] && grep -q 'platform:el8' /etc/os-release ; then dnf makecache && dnf update -y && dnf --assumeyes install python3 sudo python3-devel bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && rm -Rf /usr/share/doc && rm -Rf /usr/share/man && dnf clean all && cp /bin/true /sbin/agetty; \
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash iproute && dnf clean all; \
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml iproute2 && zypper clean -a; \
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates iproute2 && xbps-remove -O; fi

View File

@@ -0,0 +1,64 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-data:
line-length: disable
platforms:
- name: centos_7
hostname: centos_7
image: centos:7
command: /sbin/init
privileged: True
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: ubuntu_16
hostname: ubuntu_16
image: ubuntu:16.04
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: ubuntu_18
hostname: ubuntu_18
image: ubuntu:18.04
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: debian_buster
hostname: debian_buster
image: debian:buster
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: debian_stretch
hostname: debian_stretch
image: debian:stretch
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
provisioner:
name: ansible
lint:
name: ansible-lint
enabled: false
verifier:
name: testinfra
lint:
name: flake8
options:
ignore: 'E501'

View File

@@ -0,0 +1,39 @@
---
- name: Converge
hosts: all
roles:
- role: mongodb_repository
tags: molecule-idempotence-notest
- role: mongodb_config
vars:
db_path: "/data/db"
tasks:
- name: Install python stuff
package:
name: ["python-setuptools", "python-pip"]
when: ansible_hostname == "ubuntu_16"
- name: Install pymongo
pip:
name: "{{ 'pymongo==3.12.*' if ansible_python_version[0:3] | float < 3.6 else 'pymongo' }}"
when: ansible_hostname == "ubuntu_16"
- name: Install MongoDB Shell
package:
name: mongodb-org-shell
- name: Init config server replicaset
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 27019
replica_set: cfg
validate: no
members:
- centos_7:27019
- ubuntu_16:27019
- ubuntu_18:27019
- debian_stretch:27019
- debian_buster:27019
when: ansible_hostname == "ubuntu_16"

View File

@@ -0,0 +1,81 @@
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def include_vars(host):
if host.system_info.distribution == "redhat" \
or host.system_info.distribution == "centos":
ansible = host.ansible('include_vars',
'file="../../vars/RedHat.yml"',
False,
False)
if host.system_info.distribution == "debian" \
or host.system_info.distribution == "ubuntu":
ansible = host.ansible('include_vars',
'file="../../vars/Debian.yml"',
False,
False)
return ansible
def test_mongod_cnf_file(host):
mongodb_user = include_vars(host)['ansible_facts']['mongodb_user']
mongodb_group = include_vars(host)['ansible_facts']['mongodb_group']
f = host.file('/etc/mongod.conf')
assert f.exists
assert f.user == mongodb_user
assert f.group == mongodb_group
def test_mongod_service(host):
mongod_service = include_vars(host)['ansible_facts']['mongod_service']
s = host.service(mongod_service)
assert s.is_running
assert s.is_enabled
def test_mongod_port(host):
port = include_vars(host)['ansible_facts']['config_port']
s = host.socket("tcp://0.0.0.0:{0}".format(port))
assert s.is_listening
def test_mongod_replicaset(host):
'''
Ensure that the MongoDB config replicaset has been created successfully
'''
port = include_vars(host)['ansible_facts']['config_port']
cmd = "mongo --port {0} --eval 'rs.status()'".format(port)
# We only want to run this once
if host.ansible.get_variables()['inventory_hostname'] == "ubuntu_16":
r = host.run(cmd)
assert "cfg" in r.stdout
assert "centos_7:{0}".format(port) in r.stdout
assert "ubuntu_16:{0}".format(port) in r.stdout
assert "ubuntu_18:{0}".format(port) in r.stdout
assert "debian_buster:{0}".format(port) in r.stdout
assert "debian_stretch:{0}".format(port) in r.stdout
def test_mongod_config_custom_path(host):
'''
Ensure that the custom path is respected
'''
default_path = "/data/db"
# assert path exists
f = host.file(default_path)
assert f.exists
assert f.is_directory
# assert mongodb.conf contains path
conf = host.file('/etc/mongod.conf').content_string
assert "dbPath: {0}".format(default_path) in conf

View File

@@ -0,0 +1,23 @@
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
{% if item.env is defined %}
{% for var, value in item.env.items() %}
{% if value %}
ENV {{ var }} {{ value }}
{% endif %}
{% endfor %}
{% endif %}
# Add systemd-sysv package for Debian to get systemd working (and procps for sysctl) and netbase for firewalld
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates iproute2 systemd-sysv procps netbase && apt-get clean; \
elif [ $(command -v dnf) ] && grep -q 'platform:el8' /etc/os-release ; then dnf makecache && dnf update -y && dnf --assumeyes install python3 sudo python3-devel bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && rm -Rf /usr/share/doc && rm -Rf /usr/share/man && dnf clean all && cp /bin/true /sbin/agetty; \
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash iproute && dnf clean all; \
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml iproute2 && zypper clean -a; \
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates iproute2 && xbps-remove -O; fi

View File

@@ -0,0 +1,64 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-data:
line-length: disable
platforms:
- name: centos_7
hostname: centos_7
image: centos:7
command: /sbin/init
privileged: True
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: ubuntu_16
hostname: ubuntu_16
image: ubuntu:16.04
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: ubuntu_18
hostname: ubuntu_18
image: ubuntu:18.04
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: debian_buster
hostname: debian_buster
image: debian:buster
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
- name: debian_stretch
hostname: debian_stretch
image: debian:stretch
command: /sbin/init
privileged: True
networks:
- name: "mymongo"
network_mode: "mymongo"
provisioner:
name: ansible
lint:
name: ansible-lint
enabled: false
verifier:
name: testinfra
lint:
name: flake8
options:
ignore: 'E501'

View File

@@ -0,0 +1,37 @@
---
- name: Converge
hosts: all
roles:
- role: mongodb_repository
tags: molecule-idempotence-notest
- role: mongodb_config
tasks:
- name: Install python stuff
package:
name: ["python-setuptools", "python-pip"]
when: ansible_hostname == "ubuntu_16"
- name: Install pymongo
pip:
name: "{{ 'pymongo==3.12.*' if ansible_python_version[0:3] | float < 3.6 else 'pymongo' }}"
when: ansible_hostname == "ubuntu_16"
- name: Install MongoDB Shell
package:
name: mongodb-org-shell
- name: Init config server replicaset
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 27019
replica_set: cfg
validate: no
members:
- centos_7:27019
- ubuntu_16:27019
- ubuntu_18:27019
- debian_stretch:27019
- debian_buster:27019
when: ansible_hostname == "ubuntu_16"

View File

@@ -0,0 +1,86 @@
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def include_vars(host):
if host.system_info.distribution == "redhat" \
or host.system_info.distribution == "centos":
ansible = host.ansible('include_vars',
'file="../../vars/RedHat.yml"',
False,
False)
if host.system_info.distribution == "debian" \
or host.system_info.distribution == "ubuntu":
ansible = host.ansible('include_vars',
'file="../../vars/Debian.yml"',
False,
False)
return ansible
def test_mongod_cnf_file(host):
mongodb_user = include_vars(host)['ansible_facts']['mongodb_user']
mongodb_group = include_vars(host)['ansible_facts']['mongodb_group']
f = host.file('/etc/mongod.conf')
assert f.exists
assert f.user == mongodb_user
assert f.group == mongodb_group
def test_mongod_service(host):
mongod_service = include_vars(host)['ansible_facts']['mongod_service']
s = host.service(mongod_service)
assert s.is_running
assert s.is_enabled
def test_mongod_port(host):
port = include_vars(host)['ansible_facts']['config_port']
s = host.socket("tcp://0.0.0.0:{0}".format(port))
assert s.is_listening
def test_mongod_replicaset(host):
'''
Ensure that the MongoDB config replicaset has been created successfully
'''
port = include_vars(host)['ansible_facts']['config_port']
cmd = "mongo --port {0} --eval 'rs.status()'".format(port)
# We only want to run this once
if host.ansible.get_variables()['inventory_hostname'] == "ubuntu_16":
r = host.run(cmd)
assert "cfg" in r.stdout
assert "centos_7:{0}".format(port) in r.stdout
assert "ubuntu_16:{0}".format(port) in r.stdout
assert "ubuntu_18:{0}".format(port) in r.stdout
assert "debian_buster:{0}".format(port) in r.stdout
assert "debian_stretch:{0}".format(port) in r.stdout
def test_mongod_config_default_path(host):
'''
Ensure that the default paths for RedHat and Debian based OSes are respected
'''
hostname = host.ansible.get_variables()['inventory_hostname']
default_path = "/var/lib/mongo"
if hostname.startswith('centos'):
default_path = "/var/lib/mongo"
elif hostname.startswith('ubuntu') or hostname.startswith('debian'):
default_path = "/var/lib/mongodb"
# assert path exists
f = host.file(default_path)
assert f.exists
assert f.is_directory
# asset mongodb.cnf contains path
conf = host.file('/etc/mongod.conf').content_string
assert "dbPath: {0}".format(default_path) in conf

Some files were not shown because too many files have changed in this diff Show More