Skip to content

Commit

Permalink
Add basic support for MongoDB Atlas (#612)
Browse files Browse the repository at this point in the history
* add Atlas plugins from existing collection

Signed-off-by: Martin Schurz <[email protected]>

* fix underscore in README

Signed-off-by: Martin Schurz <[email protected]>

* fix typo

Signed-off-by: Martin Schurz <[email protected]>

* add integration tests

Signed-off-by: Martin Schurz <[email protected]>

* convert shared variables to underscore

Signed-off-by: Martin Schurz <[email protected]>

* change options to underscore (whitelist)

Signed-off-by: Martin Schurz <[email protected]>

* change options to underscore (cluster)

Signed-off-by: Martin Schurz <[email protected]>

* change options to underscore (user)

Signed-off-by: Martin Schurz <[email protected]>

* fix role API call

Signed-off-by: Martin Schurz <[email protected]>

* fix cluster module

Signed-off-by: Martin Schurz <[email protected]>

* add tests for old parameters

Signed-off-by: Martin Schurz <[email protected]>

* fix sanity

Signed-off-by: Martin Schurz <[email protected]>

* fix sanity

Signed-off-by: Martin Schurz <[email protected]>

* use length to gate tests

Signed-off-by: Martin Schurz <[email protected]>

---------

Signed-off-by: Martin Schurz <[email protected]>
  • Loading branch information
schurzi authored Nov 27, 2023
1 parent 764a82b commit 02e5981
Show file tree
Hide file tree
Showing 14 changed files with 1,297 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ These modules are only useful for sharded MongoDB clusters:
- `community.mongodb.mongodb_shard_tag`: Manage Shard Tags.
- `community.mongodb.mongodb_shard_zone`: Manage Shard Zones.

These modules are only useful for MongoDB Atlas clusters:

- `community.mongodb.mongodb_atlas_cluster`: Manage MongoDB clusters in Atlas.
- `community.mongodb.mongodb_atlas_ldap_user`: Manage LDAP users in Atlas.
- `community.mongodb.mongodb_atlas_user`: Manage users in Atlas.
- `community.mongodb.mongodb_atlas_whitelist`: Manage IP whitelists in Atlas.

## community.mongodb Role Tags

Expand Down
1 change: 1 addition & 0 deletions galaxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ authors:
- Loic Blot (http://www.infopro-digital.com/)
- Matt Martz (https://github.com/sivel)
- Jacob Floyd (https://github.com/cognifloyd)
- Martin Schurz (https://github.com/schurzi)
description: MongoDB related Ansible Roles, Modules, and Plugins
license_file: COPYING
tags:
Expand Down
54 changes: 54 additions & 0 deletions plugins/doc_fragments/atlas_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 T-Systems MMS
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# This module 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 software 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 software. If not, see <http://www.gnu.org/licenses/>.

from __future__ import absolute_import, division, print_function

__metaclass__ = type


class ModuleDocFragment(object):
# Documentation for global options that are always the same
DOCUMENTATION = r'''
options:
api_username:
description:
- The username for use in authentication with the Atlas API.
- Can use API users and tokens (public key is username)
type: str
required: True
aliases: [apiUsername]
api_password:
description:
- The password for use in authentication with the Atlas API.
- Can use API users and tokens (private key is password)
type: str
required: True
aliases: [apiPassword]
group_id:
description:
- Unique identifier for the Atlas project.
type: str
required: True
aliases: [groupId]
state:
description:
- State of the ressource.
choices: [ "present", "absent" ]
default: present
type: str
'''
220 changes: 220 additions & 0 deletions plugins/module_utils/mongodb_atlas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function

__metaclass__ = type

import json
from collections import defaultdict

from ansible.module_utils.urls import fetch_url

try:
from urllib import quote
except ImportError:
# noinspection PyCompatibility, PyUnresolvedReferences
from urllib.parse import (
quote,
) # pylint: disable=locally-disabled, import-error, no-name-in-module


class AtlasAPIObject:
module = None

def __init__(
self, module, object_name, group_id, path, data, data_is_array=False
):
self.module = module
self.path = path
self.data = data
self.group_id = group_id
self.object_name = object_name
self.data_is_array = data_is_array

self.module.params["url_username"] = self.module.params["api_username"]
self.module.params["url_password"] = self.module.params["api_password"]

def call_url(self, path, data="", method="GET"):
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}

if self.data_is_array and data != "":
data = "[" + data + "]"

url = (
"https://cloud.mongodb.com/api/atlas/v1.0/groups/"
+ self.group_id
+ path
)
rsp, info = fetch_url(
module=self.module,
url=url,
data=data,
headers=headers,
method=method,
)

content = ""
error = ""
if rsp and info["status"] not in (204, 404):
content = json.loads(rsp.read())
if info["status"] >= 400:
try:
content = json.loads(info["body"])
error = content["reason"]
if "detail" in content:
error += ". Detail: " + content["detail"]
except ValueError:
error = info["msg"]
if info["status"] < 0:
error = info["msg"]
return {"code": info["status"], "data": content, "error": error}

def exists(self):
additional_path = ""
if self.path == "/databaseUsers":
additional_path = "/admin"
ret = self.call_url(
path=self.path
+ additional_path
+ "/"
+ quote(self.data[self.object_name], "")
)
if ret["code"] == 200:
return True
return False

def create(self):
ret = self.call_url(
path=self.path,
data=self.module.jsonify(self.data),
method="POST",
)
return ret

def delete(self):
additional_path = ""
if self.path == "/databaseUsers":
additional_path = "/admin"
ret = self.call_url(
path=self.path
+ additional_path
+ "/"
+ quote(self.data[self.object_name], ""),
method="DELETE",
)
return ret

def modify(self):
additional_path = ""
if self.path == "/databaseUsers":
additional_path = "/admin"
ret = self.call_url(
path=self.path
+ additional_path
+ "/"
+ quote(self.data[self.object_name], ""),
data=self.module.jsonify(self.data),
method="PATCH",
)
return ret

def diff(self):
additional_path = ""
if self.path == "/databaseUsers":
additional_path = "/admin"
ret = self.call_url(
path=self.path
+ additional_path
+ "/"
+ quote(self.data[self.object_name], ""),
method="GET",
)

data_from_atlas = json.loads(self.module.jsonify(ret["data"]))
data_from_task = json.loads(self.module.jsonify(self.data))

diff = defaultdict(dict)
for key, value in data_from_atlas.items():
if key in data_from_task.keys() and value != data_from_task[key]:
diff["before"][key] = "{val}".format(val=value)
diff["after"][key] = "{val}".format(val=data_from_task[key])
return diff

def update(self, state):
changed = False
diff_result = {"before": "", "after": ""}
if self.exists():
diff_result.update({"before": "state: present\n"})
if state == "absent":
if self.module.check_mode:
diff_result.update({"after": "state: absent\n"})
self.module.exit_json(
changed=True,
object_name=self.data[self.object_name],
diff=diff_result,
)
else:
try:
ret = self.delete()
if ret["code"] == 204 or ret["code"] == 202:
changed = True
diff_result.update({"after": "state: absent\n"})
else:
self.module.fail_json(
msg="bad return code while deleting: %d. Error message: %s"
% (ret["code"], ret["error"])
)
except Exception as e:
self.module.fail_json(
msg="exception when deleting: " + str(e)
)

else:
diff_result.update(self.diff())
if self.module.check_mode:
if diff_result["after"] != "":
changed = True
self.module.exit_json(
changed=changed,
object_name=self.data[self.object_name],
data=self.data,
diff=diff_result,
)
if diff_result["after"] != "":
if self.path == "/whitelist":
ret = self.create()
else:
ret = self.modify()
if ret["code"] == 200 or ret["code"] == 201:
changed = True
else:
self.module.fail_json(
msg="bad return code while modifying: %d. Error message: %s"
% (ret["code"], ret["error"])
)

else:
diff_result.update({"before": "state: absent\n"})
if state == "present":
if self.module.check_mode:
changed = True
diff_result.update({"after": "state: created\n"})
else:
try:
ret = self.create()
if ret["code"] == 201:
changed = True
diff_result.update({"after": "state: created\n"})
else:
self.module.fail_json(
msg="bad return code while creating: %d. Error message: %s"
% (ret["code"], ret["error"])
)
except Exception as e:
self.module.fail_json(
msg="exception while creating: " + str(e)
)
return changed, diff_result
Loading

0 comments on commit 02e5981

Please sign in to comment.