346 lines
10 KiB
Python
346 lines
10 KiB
Python
#!/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()
|