mirror of
https://github.com/ansible-collections/community.mysql.git
synced 2025-05-19 23:49:10 -07:00
Some checks failed
Plugins CI / Sanity (Ⓐdevel) (push) Has been cancelled
Plugins CI / Sanity (Ⓐstable-2.16) (push) Has been cancelled
Plugins CI / Sanity (Ⓐstable-2.17) (push) Has been cancelled
Plugins CI / Sanity (Ⓐstable-2.18) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mysql 8.4.1, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mariadb 11.4.5, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐdevel, DB: mysql 8.4.1, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.0.38, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 10.11.8, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.0.38, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 10.11.8, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.0.38, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.4.1, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 10.11.8, connector: pymysql 0.10.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.0.38, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.0.38, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mariadb 11.4.5, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.16, DB: mysql 8.4.1, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mysql 8.4.1, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mariadb 11.4.5, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.17, DB: mysql 8.4.1, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.0.38, connector: mysqlclient 2.0.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 10.11.8, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.0.38, connector: mysqlclient 2.0.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 10.11.8, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.0.38, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.4.1, connector: mysqlclient 2.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.0.38, connector: pymysql 0.9.3) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.0.38, connector: pymysql 1.0.2) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mariadb 11.4.5, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Integration (Ⓐstable-2.18, DB: mysql 8.4.1, connector: pymysql 1.1.1) (push) Has been cancelled
Plugins CI / Units (Ⓐdevel, Python3.10) (push) Has been cancelled
Plugins CI / Units (Ⓐdevel, Python3.11) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.16, Python3.9) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.17, Python3.10) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.17, Python3.11) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.18, Python3.10) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.18, Python3.11) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.18, Python3.8) (push) Has been cancelled
Plugins CI / Units (Ⓐstable-2.18, Python3.9) (push) Has been cancelled
* tests: grant a privilege to the PUBLIC role to force the module to fail * skip the PUBLIC role from the users migration example * Cut the 3rd matching group as it fails to match roles without quotes The group purpose was to match the closing quotes. We don't need the value after the TO. So we can ignore this entirely. * add MariaDB role detection * Add changelog fragment * doc: add notes that users_info doesn't support MariaDB roles.
803 lines
27 KiB
Python
803 lines
27 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.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: mysql_info
|
|
short_description: Gather information about MySQL or MariaDB servers
|
|
description:
|
|
- Gathers information about MySQL or MariaDB servers.
|
|
|
|
options:
|
|
filter:
|
|
description:
|
|
- Limit the collected information by comma separated string or YAML list.
|
|
- Allowable values are C(version), C(databases), C(settings), C(global_status),
|
|
C(users), C(users_info), C(engines), C(master_status), C(slave_status), C(slave_hosts).
|
|
- By default, collects all subsets.
|
|
- You can use '!' before value (for example, C(!settings)) to exclude it from the information.
|
|
- If you pass including and excluding values to the filter, for example, I(filter=!settings,version),
|
|
the excluding values, C(!settings) in this case, will be ignored.
|
|
type: list
|
|
elements: str
|
|
login_db:
|
|
description:
|
|
- Database name to connect to.
|
|
- It makes sense if I(login_user) is allowed to connect to a specific database only.
|
|
type: str
|
|
exclude_fields:
|
|
description:
|
|
- List of fields which are not needed to collect.
|
|
- "Supports elements: C(db_size), C(db_table_count). Unsupported elements will be ignored."
|
|
type: list
|
|
elements: str
|
|
version_added: '0.1.0'
|
|
return_empty_dbs:
|
|
description:
|
|
- Includes names of empty databases to returned dictionary.
|
|
type: bool
|
|
default: false
|
|
|
|
notes:
|
|
- Compatible with MariaDB or MySQL.
|
|
- Calculating the size of a database might be slow, depending on the number and size of tables in it.
|
|
To avoid this, use I(exclude_fields=db_size).
|
|
- filters C(users_info) doesn't support MariaDB roles.
|
|
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
|
|
seealso:
|
|
- module: community.mysql.mysql_variables
|
|
- module: community.mysql.mysql_db
|
|
- module: community.mysql.mysql_user
|
|
- module: community.mysql.mysql_replication
|
|
|
|
author:
|
|
- Andrew Klychkov (@Andersson007)
|
|
- Sebastian Gumprich (@rndmh3ro)
|
|
- Laurent Indermühle (@laurent-indermuehle)
|
|
|
|
extends_documentation_fragment:
|
|
- community.mysql.mysql
|
|
'''
|
|
|
|
EXAMPLES = r'''
|
|
# Display info from mysql-hosts group (using creds from ~/.my.cnf to connect):
|
|
# ansible mysql-hosts -m mysql_info
|
|
|
|
# Display only databases and users info:
|
|
# ansible mysql-hosts -m mysql_info -a 'filter=databases,users'
|
|
|
|
# Display all users privileges:
|
|
# ansible mysql-hosts -m mysql_info -a 'filter=users_info'
|
|
|
|
# Display only slave status:
|
|
# ansible standby -m mysql_info -a 'filter=slave_status'
|
|
|
|
# Display all info from databases group except settings:
|
|
# ansible databases -m mysql_info -a 'filter=!settings'
|
|
|
|
# If you encounter the "Please explicitly state intended protocol" error,
|
|
# use the login_unix_socket argument
|
|
- name: Collect all possible information using passwordless root access
|
|
community.mysql.mysql_info:
|
|
login_user: root
|
|
login_unix_socket: /run/mysqld/mysqld.sock
|
|
|
|
- name: Get MySQL version with non-default credentials
|
|
community.mysql.mysql_info:
|
|
login_user: mysuperuser
|
|
login_password: mysuperpass
|
|
filter: version
|
|
|
|
- name: Collect all info except settings and users by root
|
|
community.mysql.mysql_info:
|
|
login_user: root
|
|
login_password: rootpass
|
|
filter: "!settings,!users"
|
|
|
|
- name: Collect info about databases and version using ~/.my.cnf as a credential file
|
|
become: true
|
|
community.mysql.mysql_info:
|
|
filter:
|
|
- databases
|
|
- version
|
|
|
|
- name: Collect info about databases and version using ~alice/.my.cnf as a credential file
|
|
become: true
|
|
community.mysql.mysql_info:
|
|
config_file: /home/alice/.my.cnf
|
|
filter:
|
|
- databases
|
|
- version
|
|
|
|
- name: Collect info about databases including empty and excluding their sizes
|
|
become: true
|
|
community.mysql.mysql_info:
|
|
config_file: /home/alice/.my.cnf
|
|
filter:
|
|
- databases
|
|
exclude_fields: db_size
|
|
return_empty_dbs: true
|
|
|
|
- name: Clone users from one server to another
|
|
block:
|
|
# Step 1
|
|
- name: Fetch information from a source server
|
|
delegate_to: server_source
|
|
community.mysql.mysql_info:
|
|
filter:
|
|
- users_info
|
|
register: result
|
|
|
|
# Step 2
|
|
# Don't work with sha256_password and cache_sha2_password
|
|
- name: Clone users fetched in a previous task to a target server
|
|
community.mysql.mysql_user:
|
|
name: "{{ item.name }}"
|
|
host: "{{ item.host }}"
|
|
plugin: "{{ item.plugin | default(omit) }}"
|
|
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
|
|
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
|
|
tls_requires: "{{ item.tls_requires | default(omit) }}"
|
|
priv: "{{ item.priv | default(omit) }}"
|
|
resource_limits: "{{ item.resource_limits | default(omit) }}"
|
|
locked: "{{ item.locked | default(omit) }}"
|
|
column_case_sensitive: true
|
|
state: present
|
|
loop: "{{ result.users_info }}"
|
|
loop_control:
|
|
label: "{{ item.name }}@{{ item.host }}"
|
|
when:
|
|
- item.name != 'root' # In case you don't want to import admin accounts
|
|
- item.name != 'mariadb.sys'
|
|
- item.name != 'mysql'
|
|
- item.name != 'PUBLIC' # MariaDB roles are not supported
|
|
'''
|
|
|
|
RETURN = r'''
|
|
server_engine:
|
|
description: Database server engine.
|
|
returned: if not excluded by filter
|
|
type: str
|
|
sample: 'MariaDB'
|
|
version_added: '3.10.0'
|
|
version:
|
|
description: Database server version.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample: { "version": { "major": 5, "minor": 5, "release": 60, "suffix": "MariaDB", "full": "5.5.60-MariaDB" } }
|
|
contains:
|
|
major:
|
|
description: Major server version.
|
|
returned: if not excluded by filter
|
|
type: int
|
|
sample: 5
|
|
minor:
|
|
description: Minor server version.
|
|
returned: if not excluded by filter
|
|
type: int
|
|
sample: 5
|
|
release:
|
|
description: Release server version.
|
|
returned: if not excluded by filter
|
|
type: int
|
|
sample: 60
|
|
suffix:
|
|
description: Server suffix, for example MySQL, MariaDB, other or none.
|
|
returned: if not excluded by filter
|
|
type: str
|
|
sample: "MariaDB"
|
|
full:
|
|
description: Full server version.
|
|
returned: if not excluded by filter
|
|
type: str
|
|
sample: "5.5.60-MariaDB"
|
|
databases:
|
|
description: Information about databases.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "mysql": { "size": 656594, "tables": 31 }, "information_schema": { "size": 73728, "tables": 79 } }
|
|
contains:
|
|
size:
|
|
description: Database size in bytes.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample: { 'size': 656594 }
|
|
tables:
|
|
description: Count of tables and views in that database.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample: { 'tables': 12 }
|
|
version_added: '3.11.0'
|
|
settings:
|
|
description: Global settings (variables) information.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "innodb_open_files": 300, innodb_page_size": 16384 }
|
|
global_status:
|
|
description: Global status information.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 }
|
|
users:
|
|
description: Return a dictionnary of users grouped by host and with global privileges only.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } }
|
|
users_info:
|
|
description:
|
|
- Information about users accounts.
|
|
- The output can be used as an input of the M(community.mysql.mysql_user) plugin.
|
|
- Useful when migrating accounts to another server or to create an inventory.
|
|
- Does not support proxy privileges. If an account has proxy privileges, they won't appear in the output.
|
|
- Causes issues with authentications plugins C(sha256_password) and C(caching_sha2_password).
|
|
If the output is fed to M(community.mysql.mysql_user), the
|
|
``plugin_auth_string`` will most likely be unreadable due to non-binary
|
|
characters.
|
|
- The "locked" field was aded in ``community.mysql`` 3.13.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "plugin_auth_string": '*1234567',
|
|
"name": "user1",
|
|
"host": "host.com",
|
|
"plugin": "mysql_native_password",
|
|
"priv": "db1.*:SELECT/db2.*:SELECT",
|
|
"resource_limits": { "MAX_USER_CONNECTIONS": 100 },
|
|
"tls_requires": { "SSL": null },
|
|
"locked": false }
|
|
version_added: '3.8.0'
|
|
engines:
|
|
description: Information about the server's storage engines.
|
|
returned: if not excluded by filter
|
|
type: dict
|
|
sample:
|
|
- { "CSV": { "Comment": "CSV storage engine", "Savepoints": "NO", "Support": "YES", "Transactions": "NO", "XA": "NO" } }
|
|
master_status:
|
|
description: Master status information.
|
|
returned: if master
|
|
type: dict
|
|
sample:
|
|
- { "Binlog_Do_DB": "", "Binlog_Ignore_DB": "mysql", "File": "mysql-bin.000001", "Position": 769 }
|
|
slave_status:
|
|
description: Slave status information.
|
|
returned: if standby
|
|
type: dict
|
|
sample:
|
|
- { "192.168.1.101": { "3306": { "replication_user": { "Connect_Retry": 60, "Exec_Master_Log_Pos": 769, "Last_Errno": 0 } } } }
|
|
slave_hosts:
|
|
description: Slave status information.
|
|
returned: if master
|
|
type: dict
|
|
sample:
|
|
- { "2": { "Host": "", "Master_id": 1, "Port": 3306 } }
|
|
connector_name:
|
|
description: Name of the python connector used by the module. When the connector is not identified, returns C(Unknown).
|
|
returned: always
|
|
type: str
|
|
sample:
|
|
- "pymysql"
|
|
version_added: '3.6.0'
|
|
connector_version:
|
|
description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown).
|
|
returned: always
|
|
type: str
|
|
sample:
|
|
- "1.0.2"
|
|
version_added: '3.6.0'
|
|
'''
|
|
|
|
from decimal import Decimal
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
|
|
CommandResolver
|
|
)
|
|
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
|
|
mysql_connect,
|
|
mysql_common_argument_spec,
|
|
mysql_driver,
|
|
mysql_driver_fail_msg,
|
|
get_connector_name,
|
|
get_connector_version,
|
|
get_server_implementation,
|
|
get_server_version,
|
|
)
|
|
|
|
from ansible_collections.community.mysql.plugins.module_utils.user import (
|
|
privileges_get,
|
|
get_resource_limits,
|
|
get_existing_authentication,
|
|
get_user_implementation,
|
|
user_is_locked,
|
|
)
|
|
from ansible.module_utils.six import iteritems
|
|
from ansible.module_utils._text import to_native
|
|
|
|
|
|
# ===========================================
|
|
# MySQL module specific support methods.
|
|
#
|
|
|
|
class MySQL_Info(object):
|
|
|
|
"""Class for collection MySQL instance information.
|
|
|
|
Arguments:
|
|
module (AnsibleModule): Object of AnsibleModule class.
|
|
cursor (pymysql/mysql-python): Cursor class for interaction with
|
|
the database.
|
|
|
|
Note:
|
|
If you need to add a new subset:
|
|
1. add a new key with the same name to self.info attr in self.__init__()
|
|
2. add a new private method to get the information
|
|
3. add invocation of the new method to self.__collect()
|
|
4. add info about the new subset to the DOCUMENTATION block
|
|
5. add info about the new subset with an example to RETURN block
|
|
"""
|
|
|
|
def __init__(self, module, cursor, server_implementation, server_version, user_implementation):
|
|
self.module = module
|
|
self.cursor = cursor
|
|
self.server_implementation = server_implementation
|
|
self.server_version = server_version
|
|
self.user_implementation = user_implementation
|
|
self.command_resolver = CommandResolver(self.server_implementation, self.server_version)
|
|
self.info = {
|
|
'version': {},
|
|
'databases': {},
|
|
'settings': {},
|
|
'global_status': {},
|
|
'engines': {},
|
|
'users': {},
|
|
'users_info': {},
|
|
'master_status': {},
|
|
'slave_hosts': {},
|
|
'slave_status': {},
|
|
}
|
|
|
|
def get_info(self, filter_, exclude_fields, return_empty_dbs):
|
|
"""Get MySQL instance information based on filter_.
|
|
|
|
Arguments:
|
|
filter_ (list): List of collected subsets (e.g., databases, users, etc.),
|
|
when it is empty, return all available information.
|
|
"""
|
|
|
|
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:
|
|
self.__collect(exclude_fields, return_empty_dbs, set(inc_list))
|
|
|
|
for i in self.info:
|
|
if i in inc_list:
|
|
partial_info[i] = self.info[i]
|
|
|
|
else:
|
|
not_in_exc_list = list(set(self.info) - set(exc_list))
|
|
self.__collect(exclude_fields, return_empty_dbs, set(not_in_exc_list))
|
|
|
|
for i in self.info:
|
|
if i not in exc_list:
|
|
partial_info[i] = self.info[i]
|
|
|
|
return partial_info
|
|
|
|
else:
|
|
self.__collect(exclude_fields, return_empty_dbs, set(self.info))
|
|
return self.info
|
|
|
|
def __collect(self, exclude_fields, return_empty_dbs, wanted):
|
|
"""Collect all possible subsets."""
|
|
if 'version' in wanted or 'settings' in wanted:
|
|
self.__get_global_variables()
|
|
|
|
if 'databases' in wanted:
|
|
self.__get_databases(exclude_fields, return_empty_dbs)
|
|
|
|
if 'global_status' in wanted:
|
|
self.__get_global_status()
|
|
|
|
if 'engines' in wanted:
|
|
self.__get_engines()
|
|
|
|
if 'users' in wanted:
|
|
self.__get_users()
|
|
|
|
if 'users_info' in wanted:
|
|
self.__get_users_info()
|
|
|
|
if 'master_status' in wanted:
|
|
self.__get_master_status()
|
|
|
|
if 'slave_status' in wanted:
|
|
self.__get_slave_status()
|
|
|
|
if 'slave_hosts' in wanted:
|
|
self.__get_slaves()
|
|
|
|
def __get_engines(self):
|
|
"""Get storage engines info."""
|
|
res = self.__exec_sql('SHOW ENGINES')
|
|
|
|
if res:
|
|
for line in res:
|
|
engine = line['Engine']
|
|
self.info['engines'][engine] = {}
|
|
|
|
for vname, val in iteritems(line):
|
|
if vname != 'Engine':
|
|
self.info['engines'][engine][vname] = val
|
|
|
|
def __convert(self, val):
|
|
"""Convert unserializable data."""
|
|
try:
|
|
if isinstance(val, Decimal):
|
|
val = float(val)
|
|
else:
|
|
val = int(val)
|
|
|
|
except ValueError:
|
|
pass
|
|
|
|
except TypeError:
|
|
pass
|
|
|
|
return val
|
|
|
|
def __get_global_variables(self):
|
|
"""Get global variables (instance settings)."""
|
|
res = self.__exec_sql('SHOW GLOBAL VARIABLES')
|
|
|
|
if res:
|
|
for var in res:
|
|
self.info['settings'][var['Variable_name']] = self.__convert(var['Value'])
|
|
|
|
# version = ["5", "5," "60-MariaDB]
|
|
version = self.info['settings']['version'].split('.')
|
|
|
|
# full_version = "5.5.60-MariaDB"
|
|
full = self.info['settings']['version']
|
|
|
|
# release = "60"
|
|
release = version[2].split('-')[0]
|
|
|
|
# check if a suffix exists by counting the length
|
|
if len(version[2].split('-')) > 1:
|
|
# suffix = "MariaDB"
|
|
suffix = version[2].split('-', 1)[1]
|
|
else:
|
|
suffix = ""
|
|
|
|
self.info['version'] = dict(
|
|
# major = "5"
|
|
major=int(version[0]),
|
|
# minor = "5"
|
|
minor=int(version[1]),
|
|
release=int(release),
|
|
suffix=str(suffix),
|
|
full=str(full),
|
|
)
|
|
|
|
def __get_global_status(self):
|
|
"""Get global status."""
|
|
res = self.__exec_sql('SHOW GLOBAL STATUS')
|
|
|
|
if res:
|
|
for var in res:
|
|
self.info['global_status'][var['Variable_name']] = self.__convert(var['Value'])
|
|
|
|
def __get_master_status(self):
|
|
"""Get master status if the instance is a master."""
|
|
query = self.command_resolver.resolve_command("SHOW MASTER STATUS")
|
|
res = self.__exec_sql(query)
|
|
if res:
|
|
for line in res:
|
|
for vname, val in iteritems(line):
|
|
self.info['master_status'][vname] = self.__convert(val)
|
|
|
|
def __get_slave_status(self):
|
|
"""Get slave status if the instance is a slave."""
|
|
query = self.command_resolver.resolve_command("SHOW SLAVE STATUS")
|
|
res = self.__exec_sql(query)
|
|
if res:
|
|
for line in res:
|
|
host = line['Master_Host']
|
|
if host not in self.info['slave_status']:
|
|
self.info['slave_status'][host] = {}
|
|
|
|
port = line['Master_Port']
|
|
if port not in self.info['slave_status'][host]:
|
|
self.info['slave_status'][host][port] = {}
|
|
|
|
user = line['Master_User']
|
|
if user not in self.info['slave_status'][host][port]:
|
|
self.info['slave_status'][host][port][user] = {}
|
|
|
|
for vname, val in iteritems(line):
|
|
if vname not in ('Master_Host', 'Master_Port', 'Master_User'):
|
|
self.info['slave_status'][host][port][user][vname] = self.__convert(val)
|
|
|
|
def __get_slaves(self):
|
|
"""Get slave hosts info if the instance is a master."""
|
|
query = self.command_resolver.resolve_command("SHOW SLAVE HOSTS")
|
|
res = self.__exec_sql(query)
|
|
if res:
|
|
for line in res:
|
|
srv_id = line['Server_id']
|
|
if srv_id not in self.info['slave_hosts']:
|
|
self.info['slave_hosts'][srv_id] = {}
|
|
|
|
for vname, val in iteritems(line):
|
|
if vname != 'Server_id':
|
|
self.info['slave_hosts'][srv_id][vname] = self.__convert(val)
|
|
|
|
def __get_users(self):
|
|
"""Get user info."""
|
|
res = self.__exec_sql('SELECT * FROM mysql.user')
|
|
if res:
|
|
for line in res:
|
|
host = line['Host']
|
|
if host not in self.info['users']:
|
|
self.info['users'][host] = {}
|
|
|
|
user = line['User']
|
|
self.info['users'][host][user] = {}
|
|
|
|
for vname, val in iteritems(line):
|
|
if vname not in ('Host', 'User'):
|
|
self.info['users'][host][user][vname] = self.__convert(val)
|
|
|
|
def __get_users_info(self):
|
|
"""Get user privileges, passwords, resources_limits, ...
|
|
|
|
Query the server to get all the users and return a string
|
|
of privileges that can be used by the mysql_user plugin.
|
|
For instance:
|
|
|
|
"users_info": [
|
|
{
|
|
"host": "users_info.com",
|
|
"priv": "*.*: ALL,GRANT",
|
|
"name": "users_info_adm"
|
|
},
|
|
{
|
|
"host": "users_info.com",
|
|
"priv": "`mysql`.*: SELECT/`users_info_db`.*: SELECT",
|
|
"name": "users_info_multi"
|
|
}
|
|
]
|
|
"""
|
|
res = self.__exec_sql('SELECT * FROM mysql.user')
|
|
if not res:
|
|
return None
|
|
|
|
output = list()
|
|
for line in res:
|
|
user = line['User']
|
|
host = line['Host']
|
|
|
|
# MariaDB roles have no host
|
|
is_role = self.server_implementation == 'mariadb' and not host
|
|
user_priv = privileges_get(self.cursor, user, host, maria_role=is_role)
|
|
|
|
if not user_priv:
|
|
self.module.warn("No privileges found for %s on host %s" % (user, host))
|
|
continue
|
|
|
|
priv_string = list()
|
|
for db_table, priv in user_priv.items():
|
|
# Proxy privileges are hard to work with because of different quotes or
|
|
# backticks like ''@'', ''@'%' or even ``@``. In addition, MySQL will
|
|
# forbid you to grant a proxy privileges through TCP.
|
|
if set(priv) == {'PROXY', 'GRANT'} or set(priv) == {'PROXY'}:
|
|
continue
|
|
|
|
unquote_db_table = db_table.replace('`', '').replace("'", '')
|
|
priv_string.append('%s:%s' % (unquote_db_table, ','.join(priv)))
|
|
|
|
# Only keep *.* USAGE if it's the only user privilege given
|
|
if len(priv_string) > 1 and '*.*:USAGE' in priv_string:
|
|
priv_string.remove('*.*:USAGE')
|
|
|
|
resource_limits = get_resource_limits(self.cursor, user, host)
|
|
copy_ressource_limits = dict.copy(resource_limits)
|
|
|
|
tls_requires = self.user_implementation.get_tls_requires(
|
|
self.cursor, user, host)
|
|
|
|
output_dict = {
|
|
'name': user,
|
|
'host': host,
|
|
'priv': '/'.join(priv_string),
|
|
'resource_limits': copy_ressource_limits,
|
|
'tls_requires': tls_requires,
|
|
}
|
|
|
|
# Prevent returning a resource limit if empty
|
|
if resource_limits:
|
|
for key, value in resource_limits.items():
|
|
if value == 0:
|
|
del output_dict['resource_limits'][key]
|
|
if len(output_dict['resource_limits']) == 0:
|
|
del output_dict['resource_limits']
|
|
|
|
# Prevent returning tls_require if empty
|
|
if not tls_requires:
|
|
del output_dict['tls_requires']
|
|
|
|
authentications = get_existing_authentication(self.cursor, user, host)
|
|
if authentications:
|
|
output_dict.update(authentications[0])
|
|
|
|
if line.get('is_role') and line['is_role'] == 'N':
|
|
output_dict['locked'] = user_is_locked(self.cursor, user, host)
|
|
|
|
# TODO password_option
|
|
# but both are not supported by mysql_user atm. So no point yet.
|
|
|
|
output.append(output_dict)
|
|
|
|
self.info['users_info'] = output
|
|
|
|
def __get_databases(self, exclude_fields, return_empty_dbs):
|
|
"""Get info about databases."""
|
|
|
|
def is_field_included(field_name):
|
|
return not exclude_fields or 'db_{}'.format(field_name) not in exclude_fields
|
|
|
|
def create_db_info(db_data):
|
|
info = {}
|
|
if is_field_included('size'):
|
|
info['size'] = int(db_data.get('size', 0) or 0)
|
|
if is_field_included('table_count'):
|
|
info['tables'] = int(db_data.get('tables', 0) or 0)
|
|
return info
|
|
|
|
# Build the main query
|
|
query_parts = ['SELECT table_schema AS "name"']
|
|
if is_field_included('size'):
|
|
query_parts.append('SUM(data_length + index_length) AS "size"')
|
|
if is_field_included('table_count'):
|
|
query_parts.append('COUNT(table_name) as "tables"')
|
|
|
|
query = "{} FROM information_schema.TABLES GROUP BY table_schema".format(", ".join(query_parts))
|
|
|
|
# Get and process databases with tables
|
|
databases = self.__exec_sql(query) or []
|
|
for db in databases:
|
|
self.info['databases'][db['name']] = create_db_info(db)
|
|
|
|
# Handle empty databases if requested
|
|
if return_empty_dbs:
|
|
empty_databases = self.__exec_sql('SHOW DATABASES') or []
|
|
for db in empty_databases:
|
|
db_name = db['Database']
|
|
if db_name not in self.info['databases']:
|
|
self.info['databases'][db_name] = create_db_info({})
|
|
|
|
def __exec_sql(self, query, ddl=False):
|
|
"""Execute SQL.
|
|
|
|
Arguments:
|
|
ddl (bool): If True, return True or False.
|
|
Used for queries that don't return any rows
|
|
(mainly for DDL queries) (default False).
|
|
"""
|
|
try:
|
|
self.cursor.execute(query)
|
|
|
|
if not ddl:
|
|
res = self.cursor.fetchall()
|
|
return res
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
|
|
return False
|
|
|
|
|
|
# ===========================================
|
|
# Module execution.
|
|
#
|
|
|
|
|
|
def main():
|
|
argument_spec = mysql_common_argument_spec()
|
|
argument_spec.update(
|
|
login_db=dict(type='str'),
|
|
filter=dict(type='list', elements='str'),
|
|
exclude_fields=dict(type='list', elements='str'),
|
|
return_empty_dbs=dict(type='bool', default=False),
|
|
)
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=argument_spec,
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
db = module.params['login_db']
|
|
connect_timeout = module.params['connect_timeout']
|
|
login_user = module.params['login_user']
|
|
login_password = module.params['login_password']
|
|
ssl_cert = module.params['client_cert']
|
|
ssl_key = module.params['client_key']
|
|
ssl_ca = module.params['ca_cert']
|
|
check_hostname = module.params['check_hostname']
|
|
config_file = module.params['config_file']
|
|
filter_ = module.params['filter']
|
|
exclude_fields = module.params['exclude_fields']
|
|
return_empty_dbs = module.params['return_empty_dbs']
|
|
|
|
if filter_:
|
|
filter_ = [f.strip() for f in filter_]
|
|
|
|
if exclude_fields:
|
|
exclude_fields = set([f.strip() for f in exclude_fields])
|
|
|
|
if mysql_driver is None:
|
|
module.fail_json(msg=mysql_driver_fail_msg)
|
|
|
|
connector_name = get_connector_name(mysql_driver)
|
|
connector_version = get_connector_version(mysql_driver)
|
|
|
|
try:
|
|
cursor, db_conn = mysql_connect(module, login_user, login_password,
|
|
config_file, ssl_cert, ssl_key, ssl_ca, db,
|
|
check_hostname=check_hostname,
|
|
connect_timeout=connect_timeout, cursor_class='DictCursor')
|
|
except Exception as e:
|
|
msg = ('unable to connect to database using %s %s, check login_user '
|
|
'and login_password are correct or %s has the credentials. '
|
|
'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e)))
|
|
module.fail_json(msg)
|
|
|
|
server_implementation = get_server_implementation(cursor)
|
|
server_version = get_server_version(cursor)
|
|
user_implementation = get_user_implementation(cursor)
|
|
|
|
###############################
|
|
# Create object and do main job
|
|
|
|
mysql = MySQL_Info(module, cursor, server_implementation, server_version, user_implementation)
|
|
|
|
module.exit_json(changed=False,
|
|
server_engine='MariaDB' if server_implementation == 'mariadb' else 'MySQL',
|
|
connector_name=connector_name,
|
|
connector_version=connector_version,
|
|
**mysql.get_info(filter_, exclude_fields, return_empty_dbs))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|