├── .gitignore ├── requirements.yml ├── .cookiecutter.yml ├── handlers └── main.yml ├── vars ├── main.yml ├── Debian-mysqld.yml ├── Debian-mariadb.yml ├── RedHat-7-mariadb.yml ├── RedHat-7-mysqld.yml ├── RedHat-8-mysqld.yml └── RedHat-8-mariadb.yml ├── templates ├── root-my.cnf.j2 ├── user-my.cnf.j2 ├── init.cnf.j2 ├── mariadb.list.j2 ├── yum.repo.j2 ├── mysql.list.j2 ├── reset.sql.j2 └── my.cnf.j2 ├── .ansible-lint ├── tasks ├── not-supported.yml ├── clear_innodb_logs.yml ├── mysql_restart.yml ├── main.yml ├── databases_and_users.yml ├── setup-Debian.yml ├── setup-RedHat.yml ├── configure.yml └── secure-installation.yml ├── molecule ├── cloud-aws-direct-mariadb │ ├── converge.yml │ └── molecule.yml ├── default │ ├── converge.yml │ ├── molecule.yml │ └── Dockerfile.j2 ├── cloud-aws-direct-5.5 │ ├── converge.yml │ └── molecule.yml ├── cloud-aws-direct-5.6 │ ├── converge.yml │ └── molecule.yml ├── cloud-aws-direct-5.7 │ ├── converge.yml │ └── molecule.yml ├── cloud-aws-direct-8.0 │ ├── converge.yml │ └── molecule.yml └── resources │ ├── prepare.yml │ └── tests │ └── verify.yml ├── .travis.yml ├── .yamllint ├── meta └── main.yml ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── documentation_report.md ├── .gitlab-ci.yml ├── defaults └── main.yml ├── README.md ├── LICENSE └── mysql_user.bak /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.rst 4 | *.log -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - community.mysql 4 | -------------------------------------------------------------------------------- /.cookiecutter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | default_context: 3 | role_name: mysql 4 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart mysql 3 | include_tasks: mysql_restart.yml 4 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | python_executable: >- 3 | {{ (ansible_facts.python.version.major|int == 3) | ternary('python3', 'python') }} 4 | -------------------------------------------------------------------------------- /templates/root-my.cnf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [client] 4 | user="{{ mysql_root_username }}" 5 | password="{{ mysql_root_password }}" 6 | -------------------------------------------------------------------------------- /templates/user-my.cnf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [client] 4 | user="{{ mysql_user_name }}" 5 | password="{{ mysql_user_password }}" 6 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - ./.travis.yml 4 | - ./molecule/ 5 | - ./.github 6 | rulesdir: 7 | - ~/ansible-lint-rules/rules/ 8 | use_default_rules: true 9 | verbosity: 1 10 | offline: true 11 | -------------------------------------------------------------------------------- /tasks/not-supported.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Warn on unsupported platform 3 | fail: 4 | msg: >- 5 | This role does not support '{{ ansible_os_family }}' platform. 6 | Please contact support@lean-delivery.com 7 | -------------------------------------------------------------------------------- /templates/init.cnf.j2: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | {% if mysql_version is version('5.7.2', '>=') and mysql_daemon in ['mysqld'] %} 3 | default_authentication_plugin = mysql_native_password 4 | {% endif %} 5 | skip-show-database 6 | skip-grant-tables -------------------------------------------------------------------------------- /templates/mariadb.list.j2: -------------------------------------------------------------------------------- 1 | deb [arch=amd64,i386,ppc64el] {{ mysql_repo }}/{{ mysql_version }}/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} main 2 | deb-src {{ mysql_repo }}/{{ mysql_version }}/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} main 3 | -------------------------------------------------------------------------------- /templates/yum.repo.j2: -------------------------------------------------------------------------------- 1 | [{{ mysql_daemon }}] 2 | name={{ mysql_daemon }} 3 | description={{ mysql_daemon }} YUM repo 4 | baseurl={{ mysql_repo }} 5 | gpgcheck=1 6 | gpgkey={{ mysql_gpgkey }} 7 | enabled=1 8 | {% if mysql_module_hotfixes|default(false)|bool %} 9 | module_hotfixes=1 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-mariadb/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: example2_db.*:ALL 17 | mysql_port: 3306 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mariadb 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os: linux 3 | dist: focal 4 | 5 | services: 6 | - docker 7 | 8 | before_install: 9 | - git clone https://github.com/lean-delivery/ansible-lint-rules.git ~/ansible-lint-rules 10 | 11 | install: 12 | - pip3 install --upgrade ansible==2.9.* docker molecule==3.* ansible-lint molecule-docker 13 | - ansible --version 14 | - ansible-lint --version 15 | 16 | script: 17 | - yamllint . -c .yamllint 18 | - ansible-lint . -c .ansible-lint 19 | - molecule test -s default 20 | 21 | notifications: 22 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ 23 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: "example2_db.*:ALL" 17 | mysql_port: 3306 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mysqld 20 | mysql_version: 5.7 21 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.5/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: "example2_db.*:ALL" 17 | mysql_port: "3306" 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mysqld 20 | mysql_version: 5.5 21 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.6/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: "example2_db.*:ALL" 17 | mysql_port: "3306" 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mysqld 20 | mysql_version: 5.6 21 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.7/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: "example2_db.*:ALL" 17 | mysql_port: "3306" 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mysqld 20 | mysql_version: 5.7 21 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-8.0/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | roles: 5 | - role: ansible-role-mysql 6 | vars: 7 | mysql_root_password: 88TEM-veDRE<888serd 8 | mysql_databases: 9 | - name: example2_db 10 | encoding: latin1 11 | collation: latin1_general_ci 12 | mysql_users: 13 | - name: example2_user 14 | host: "%" 15 | password: Sime32-SRRR-password 16 | priv: "example2_db.*:ALL" 17 | mysql_port: "3306" 18 | mysql_bind_address: '0.0.0.0' 19 | mysql_daemon: mysqld 20 | mysql_version: 8.0 21 | -------------------------------------------------------------------------------- /templates/mysql.list.j2: -------------------------------------------------------------------------------- 1 | deb http://repo.mysql.com/apt/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} mysql-apt-config 2 | deb http://repo.mysql.com/apt/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} mysql-{{ mysql_version }} 3 | deb http://repo.mysql.com/apt/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} mysql-tools 4 | deb http://repo.mysql.com/apt/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} mysql-tools-preview 5 | deb-src http://repo.mysql.com/apt/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} mysql-{{ mysql_version }} 6 | -------------------------------------------------------------------------------- /tasks/clear_innodb_logs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Stop MySQL to change configs 3 | service: 4 | name: '{{ mysql_service_name }}' 5 | state: stopped 6 | 7 | - name: Wait until the lock file is removed 8 | wait_for: 9 | path: '/var/lock/subsys/mysql' 10 | state: absent 11 | 12 | - name: Find InnoDB log files 13 | find: 14 | paths: '{{ mysql_datadir }}' 15 | patterns: 'ib_logfile*' 16 | register: innodb_log 17 | 18 | - name: Remove default InnoDB log files 19 | file: 20 | path: '{{ idb_log.path }}' 21 | state: absent 22 | loop: '{{ innodb_log.files }}' 23 | loop_control: 24 | loop_var: idb_log 25 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | max-spaces-inside: 1 7 | level: error 8 | brackets: 9 | max-spaces-inside: 1 10 | level: error 11 | colons: 12 | max-spaces-after: -1 13 | level: error 14 | commas: 15 | max-spaces-after: -1 16 | level: error 17 | empty-lines: 18 | max: 3 19 | level: error 20 | hyphens: 21 | level: error 22 | truthy: disable 23 | comments: disable 24 | comments-indentation: disable 25 | indentation: disable 26 | key-duplicates: enable 27 | line-length: 28 | max: 150 29 | level: warning 30 | new-lines: 31 | type: unix 32 | -------------------------------------------------------------------------------- /tasks/mysql_restart.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Stop and start mysql service 3 | block: 4 | - name: Stop mysql 5 | service: 6 | name: '{{ mysql_service_name }}' 7 | state: stopped 8 | 9 | - name: Wait until the lock file is removed 10 | wait_for: 11 | path: /var/lock/subsys/mysql 12 | state: absent 13 | 14 | - name: Start mysql 15 | service: 16 | name: '{{ mysql_service_name }}' 17 | state: started 18 | become: true 19 | 20 | - name: Wait for service to be ready 21 | wait_for: 22 | port: '{{ mysql_port }}' 23 | host: '{{ mysql_bind_address }}' 24 | connect_timeout: 3 25 | delay: 3 26 | timeout: 30 27 | -------------------------------------------------------------------------------- /molecule/resources/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare python3 3 | hosts: rhel7 4 | become: true 5 | vars: 6 | ansible_python_interpreter: auto 7 | tasks: 8 | 9 | - name: Add repository 10 | yum: 11 | name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 12 | state: present 13 | update_cache: true 14 | register: task_result 15 | until: task_result is succeeded 16 | 17 | - name: Install python3 18 | yum: 19 | name: 20 | - python36 21 | - python-dnf 22 | state: present 23 | update_cache: true 24 | register: task_result 25 | until: task_result is succeeded 26 | -------------------------------------------------------------------------------- /templates/reset.sql.j2: -------------------------------------------------------------------------------- 1 | {% if mysql_daemon == 'mariadb' %} 2 | {% if mysql_version is version('10.4.0', '<') %} 3 | UPDATE user SET plugin=''; 4 | UPDATE user SET password=PASSWORD('{{ mysql_root_password }}') WHERE user='root'; 5 | FLUSH PRIVILEGES; 6 | {% else %} 7 | FLUSH PRIVILEGES; 8 | SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{{ mysql_root_password }}'); 9 | {% endif %} 10 | {% elif mysql_version is version('5.7.6', '>=') %} 11 | FLUSH PRIVILEGES; 12 | ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '{{ mysql_root_password }}'; 13 | {% else %} 14 | FLUSH PRIVILEGES; 15 | SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{{ mysql_root_password }}'); 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /molecule/resources/tests/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is an example playbook to execute Ansible tests. 3 | 4 | - name: Verify 5 | hosts: all 6 | tasks: 7 | - name: Install netstat 8 | package: 9 | name: net-tools 10 | become: true 11 | 12 | - name: Populate service facts 13 | service_facts: 14 | 15 | - name: Gather facts on listening ports 16 | listen_ports_facts: 17 | 18 | - name: Assert that mysql service is running 19 | assert: 20 | that: | 21 | "'running' in ansible_facts.services['mysqld.service'].state" 22 | or 23 | "'running' in ansible_facts.services['mariadb.service'].state" 24 | or 25 | "'running' in ansible_facts.services['mysql.service'].state" 26 | 27 | - name: Assert that mysql port is listening 28 | assert: 29 | that: 30 | "3306 in (ansible_facts.tcp_listen | map(attribute='port') | list)" 31 | -------------------------------------------------------------------------------- /vars/Debian-mysqld.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mysqld 3 | mysql_apt_template: mysql.list 4 | mysql_default_packages: 5 | - mysql-server 6 | 7 | mysql_default_python_packages: 8 | - '{{ python_executable }}-pip' 9 | - '{{ python_executable }}-apt' 10 | - python3-dev 11 | - gnupg 12 | - default-libmysqlclient-dev 13 | - build-essential 14 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 15 | mysql_default_log_error: /var/log/mysql.err 16 | mysql_default_syslog_tag: mysql 17 | mysql_default_pid_file: /var/run/mysqld/mysqld.pid 18 | mysql_default_config_file: /etc/my.cnf 19 | mysql_default_config_include_dir: /etc/mysql/mysql.conf.d 20 | mysql_default_socket: /var/run/mysqld/mysqld.sock 21 | mysql_default_supports_innodb_large_prefix: false 22 | mysql_default_datadir: /var/lib/mysql 23 | mysql_default_service_name: mysql 24 | mysql_default_apt_keyserver: keyserver.ubuntu.com 25 | mysql_default_apt_key_id: 467B942D3A79BD29 # expires: 2023-12-14 26 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: mysql 4 | author: Lean Delivery Team 5 | description: MySQL server for RHEL/CentOS/Debian/Ubuntu 6 | company: EPAM Systems 7 | issue_tracker_url: https://github.com/lean-delivery/ansible-role-mysql/issues 8 | license: Apache 9 | min_ansible_version: 2.9 10 | platforms: 11 | - name: EL 12 | versions: 13 | - 7 14 | - 8 15 | - name: Ubuntu 16 | versions: 17 | - bionic 18 | - name: Debian 19 | versions: 20 | - stretch 21 | - buster 22 | versions_mysql: 23 | - name: mysql 24 | versions: 25 | - 5.5 26 | - 5.6 27 | - 5.7 28 | - 8.0 29 | - name: mariadb 30 | versions: 31 | - 10.3 32 | - 10.4 33 | - 10.5 34 | galaxy_tags: 35 | - database 36 | - mysql 37 | - mariadb 38 | - db 39 | - sql 40 | dependencies: [] 41 | collections: 42 | - community.mysql 43 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Variable configuration. 3 | - name: Include OS-specific variables 4 | include_vars: '{{ platform_vars }}' 5 | with_first_found: 6 | - "{{ ansible_os_family }}\ 7 | -{{ ansible_distribution_major_version }}\ 8 | -{{ mysql_daemon }}.yml" 9 | - '{{ ansible_os_family }}-{{ mysql_daemon }}.yml' 10 | loop_control: 11 | loop_var: platform_vars 12 | 13 | # Install Mysql 14 | - name: Configure system 15 | include_tasks: '{{ platform }}' 16 | with_first_found: 17 | - 'setup-{{ ansible_os_family }}.yml' 18 | - 'not-supported.yml' 19 | loop_control: 20 | loop_var: platform 21 | 22 | # Configure MySQL. 23 | - name: Configure 24 | include_tasks: configure.yml 25 | 26 | - name: Run secure installation 27 | include_tasks: secure-installation.yml 28 | vars: 29 | ansible_python_interpreter: /usr/bin/python3 30 | 31 | - name: Create databases and users 32 | include_tasks: databases_and_users.yml 33 | vars: 34 | ansible_python_interpreter: /usr/bin/python3 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 6 | 7 | Fixes # (issue) 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | ## Reviews 19 | 20 | Please identify developer to review this change 21 | 22 | - [ ] @developer 23 | 24 | ## Checklist: 25 | 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have made corresponding changes to the documentation 28 | - [ ] My changes generate no new warnings 29 | - [ ] I have added tests that prove my fix is effective or that my feature works 30 | - [ ] New and existing tests pass with my changes 31 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.5/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: ec2 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-aws-centos7-mysql-5.5 12 | platform: centos7 13 | instance_type: m5.large 14 | region: us-east-1 15 | vpc_subnet_id: subnet-05a2ef2b767afec50 16 | assign_public_ip: false 17 | spot_price: 0.05 18 | security_group_name: 19 | - default 20 | wait_timeout: 1800 21 | ssh_user: centos 22 | groups: 23 | - rhel_family 24 | - rhel7 25 | 26 | provisioner: 27 | name: ansible 28 | log: false 29 | config_options: 30 | defaults: 31 | callback_whitelist: profile_tasks,timer 32 | playbooks: 33 | create: ../resources/provisioning/AWS/create.yml 34 | destroy: ../resources/provisioning/AWS/destroy.yml 35 | verify: ../resources/tests/verify.yml 36 | prepare: ../resources/prepare.yml 37 | lint: 38 | name: ansible-lint 39 | scenario: 40 | name: cloud-aws-direct-5.5 41 | verifier: 42 | name: ansible 43 | -------------------------------------------------------------------------------- /vars/Debian-mariadb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mariadb 3 | mysql_default_version: 10.5 4 | mysql_default_repo: http://nyc2.mirrors.digitalocean.com/mariadb/repo 5 | mysql_apt_template: mariadb.list 6 | 7 | mysql_default_packages: 8 | - mariadb-server 9 | - libmariadb3 10 | - galera-4 11 | - mariadb-client 12 | - mariadb-common 13 | 14 | mysql_default_python_packages: 15 | - '{{ python_executable }}-pip' 16 | - dirmngr 17 | - software-properties-common 18 | - python3-dev 19 | - libmariadb-dev 20 | - build-essential 21 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 22 | mysql_default_log_error: /var/log/mariadb.log 23 | mysql_default_syslog_tag: mariadb 24 | mysql_default_pid_file: /var/run/mariadb/mariadb.pid 25 | mysql_default_config_file: /etc/my.cnf 26 | mysql_default_config_include_dir: /etc/mysql/mariadb.conf.d 27 | mysql_default_socket: /var/run/mysqld/mysqld.sock 28 | mysql_default_supports_innodb_large_prefix: true 29 | mysql_default_datadir: /var/lib/mysql 30 | mysql_default_service_name: mysql 31 | mysql_default_apt_keyserver: keyserver.ubuntu.com 32 | mysql_default_apt_key_id: '0xF1656F24C74CD1D8' 33 | -------------------------------------------------------------------------------- /tasks/databases_and_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure MySQL databases are present 3 | mysql_db: 4 | name: '{{ item.name }}' 5 | collation: '{{ item.collation | default("utf8_general_ci") }}' 6 | encoding: '{{ item.encoding | default("utf8") }}' 7 | state: '{{ item.state | default("present") }}' 8 | target: '{{ item.target | default(None) }}' 9 | login_user: '{{ mysql_root_username }}' 10 | login_password: '{{ mysql_root_password }}' 11 | loop: '{{ mysql_databases }}' 12 | become: true 13 | 14 | - name: Ensure MySQL users are present 15 | mysql_user: 16 | name: '{{ item.name }}' 17 | host: '{{ item.host | default("localhost") }}' 18 | password: '{{ item.password }}' 19 | priv: '{{ item.priv | default("*.*:USAGE") }}' 20 | state: '{{ item.state | default("present") }}' 21 | append_privs: '{{ item.append_privs | default("false") }}' 22 | encrypted: '{{ item.encrypted | default("false") }}' 23 | login_user: '{{ mysql_root_username }}' 24 | login_password: '{{ mysql_root_password }}' 25 | update_password: on_create 26 | loop: '{{ mysql_users }}' 27 | no_log: true 28 | become: true 29 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: docker 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-docker-centos7-mysql 12 | image: leandelivery/docker-systemd:centos7 13 | privileged: true 14 | groups: 15 | - rhel_family 16 | - rhel7 17 | 18 | - name: test-docker-ubuntu1804-mysql 19 | image: leandelivery/docker-systemd:ubuntu-18.04 20 | privileged: true 21 | security_opts: 22 | - seccomp=unconfined 23 | volumes: 24 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 25 | tmpfs: 26 | - /tmp 27 | - /run 28 | capabilities: 29 | - SYS_ADMIN 30 | groups: 31 | - debian_family 32 | 33 | provisioner: 34 | name: ansible 35 | log: false 36 | inventory: 37 | group_vars: 38 | pip3: 39 | ansible_python_interpreter: /usr/bin/python3 40 | playbooks: 41 | verify: ../resources/tests/verify.yml 42 | prepare: ../resources/prepare.yml 43 | scenario: 44 | name: default 45 | verifier: 46 | name: ansible 47 | -------------------------------------------------------------------------------- /molecule/default/Dockerfile.j2: -------------------------------------------------------------------------------- 1 | # Molecule managed 2 | 3 | {% if item.registry is defined %} 4 | FROM {{ item.registry.url }}/{{ item.image }} 5 | {% else %} 6 | FROM {{ item.image }} 7 | {% endif %} 8 | 9 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \ 10 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \ 11 | elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash util-linux && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ 12 | elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \ 13 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \ 14 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi 15 | 16 | RUN if [ -f /sbin/agetty ]; then cp /bin/true /sbin/agetty; \ 17 | elif [ -f /sbin/mingetty ]; then cp /bin/true /sbin/mingetty; fi 18 | -------------------------------------------------------------------------------- /vars/RedHat-7-mariadb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mariadb 3 | mysql_default_version: 10.5 4 | mysql_default_repo: "http://yum.mariadb.org/\ 5 | {{ mysql_version }}/\ 6 | centos{{ ansible_distribution_major_version }}-amd64" 7 | mysql_default_gpgkey: https://yum.mariadb.org/RPM-GPG-KEY-MariaDB 8 | mysql_default_repofile: '/etc/yum.repos.d/{{ mysql_daemon }}.repo' 9 | mysql_default_packages: 10 | - MariaDB-client 11 | - MariaDB-server 12 | - MariaDB-shared 13 | - MariaDB-common 14 | - galera-4 15 | - MariaDB-devel 16 | 17 | mysql_fingerprint: '1993 69E5 404B D5FC 7D2F E43B CBCB 082A 1BB9 43DB' 18 | mysql_default_python_packages: 19 | - boost-program-options 20 | - gcc 21 | - lsof 22 | - rsync 23 | - tar 24 | - python3-devel 25 | - python3-pip 26 | - libselinux-python3 27 | 28 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 29 | mysql_default_log_error: /var/log/mariadb.log 30 | mysql_default_syslog_tag: mariadb 31 | mysql_default_pid_file: /var/run/mariadb/mariadb.pid 32 | mysql_default_config_file: /etc/my.cnf 33 | mysql_default_config_include_dir: /etc/my.cnf.d 34 | mysql_default_socket: /var/lib/mysql/mysql.sock 35 | mysql_default_supports_innodb_large_prefix: true 36 | mysql_default_datadir: /var/lib/mysql 37 | mysql_default_service_name: mariadb 38 | -------------------------------------------------------------------------------- /vars/RedHat-7-mysqld.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mysqld 3 | mysql_default_version: 5.7 4 | mysql_default_repo: "http://repo.mysql.com/yum/\ 5 | mysql-{{ mysql_version }}-community/\ 6 | el/{{ ansible_distribution_major_version }}/$basearch/" 7 | mysql_default_gpgkey: https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 8 | mysql_default_repofile: '/etc/yum.repos.d/{{ mysql_daemon }}.repo' 9 | mysql_fingerprint: '859B E8D7 C586 F538 430B 19C2 467B 942D 3A79 BD29' 10 | mysql_default_repo_package: https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm 11 | mysql_default_packages: 12 | - mysql-community-server 13 | - mysql-community-client 14 | 15 | mysql_default_python_packages: 16 | - gcc 17 | - python3-pip 18 | - perl-DBD-MySQL 19 | - python3-devel 20 | - mysql-devel 21 | - libselinux-python3 22 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 23 | mysql_default_log_error: /var/log/mysql.err 24 | mysql_default_syslog_tag: mysql 25 | mysql_default_pid_file: /var/run/mysqld/mysqld.pid 26 | mysql_default_config_file: /etc/my.cnf 27 | mysql_default_config_include_dir: /etc/my.cnf.d 28 | mysql_default_socket: /var/lib/mysql/mysql.sock 29 | mysql_default_supports_innodb_large_prefix: false 30 | mysql_default_datadir: /var/lib/mysql 31 | mysql_default_service_name: mysqld 32 | -------------------------------------------------------------------------------- /vars/RedHat-8-mysqld.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mysqld 3 | mysql_default_version: 8.0 4 | mysql_default_repo: "http://repo.mysql.com/yum/\ 5 | mysql-{{ mysql_version }}-community/\ 6 | el/{{ ansible_distribution_major_version }}/$basearch/" 7 | mysql_default_gpgkey: https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 8 | mysql_default_repofile: '/etc/yum.repos.d/{{ mysql_daemon }}.repo' 9 | mysql_fingerprint: '859B E8D7 C586 F538 430B 19C2 467B 942D 3A79 BD29' 10 | mysql_default_repo_package: https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm 11 | mysql_default_packages: 12 | - mysql-community-server 13 | - mysql-community-client 14 | 15 | mysql_default_python_packages: 16 | - gcc 17 | - '{{ python_executable }}-pip' 18 | - perl-DBD-MySQL 19 | - python3-devel 20 | - mysql-devel 21 | - libselinux-python3 22 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 23 | mysql_default_log_error: /var/log/mysql.err 24 | mysql_default_syslog_tag: mysql 25 | mysql_default_pid_file: /var/run/mysqld/mysqld.pid 26 | mysql_default_config_file: /etc/my.cnf 27 | mysql_default_config_include_dir: /etc/my.cnf.d 28 | mysql_default_socket: /var/lib/mysql/mysql.sock 29 | mysql_default_supports_innodb_large_prefix: false 30 | mysql_default_datadir: /var/lib/mysql 31 | mysql_default_service_name: mysqld 32 | -------------------------------------------------------------------------------- /vars/RedHat-8-mariadb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_default_daemon: mariadb 3 | mysql_default_version: 10.5 4 | mysql_default_repo: 'http://yum.mariadb.org/{{ mysql_version }}/centos{{ ansible_distribution_major_version }}-amd64' 5 | mysql_default_gpgkey: https://yum.mariadb.org/RPM-GPG-KEY-MariaDB 6 | mysql_default_repofile: '/etc/yum.repos.d/{{ mysql_daemon }}.repo' 7 | mysql_module_hotfixes: true 8 | 9 | mysql_default_packages: 10 | - MariaDB-client 11 | - MariaDB-server 12 | - MariaDB-shared 13 | - MariaDB-common 14 | - galera-4 15 | - MariaDB-devel 16 | 17 | mysql_fingerprint: '1993 69E5 404B D5FC 7D2F E43B CBCB 082A 1BB9 43DB' 18 | mysql_default_python_packages: 19 | - gcc 20 | - lsof 21 | - rsync 22 | - tar 23 | - python38 24 | - python38-devel 25 | - python38-setuptools 26 | - python38-wheel 27 | - libselinux-python3 28 | 29 | mysql_default_slow_query_log_file: /var/log/mysql-slow.log 30 | mysql_default_log_error: /var/log/mariadb.log 31 | mysql_default_syslog_tag: mariadb 32 | mysql_default_pid_file: /var/run/mariadb/mariadb.pid 33 | mysql_default_config_file: /etc/my.cnf 34 | mysql_default_config_include_dir: /etc/my.cnf.d 35 | mysql_default_socket: /var/lib/mysql/mysql.sock 36 | mysql_default_supports_innodb_large_prefix: true 37 | mysql_default_datadir: /var/lib/mysql 38 | mysql_default_service_name: mariadb 39 | -------------------------------------------------------------------------------- /tasks/setup-Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add repository and install {{ mysql_version }} packages 3 | block: 4 | - name: Add universe repository for Ubuntu 5 | apt_repository: 6 | repo: deb http://archive.ubuntu.com/ubuntu bionic universe 7 | state: present 8 | when: ansible_distribution == 'Ubuntu' 9 | 10 | - name: Install requirements 11 | apt: 12 | name: '{{ mysql_python_packages }}' 13 | state: present 14 | update_cache: true 15 | register: task_result 16 | until: task_result is succeeded 17 | 18 | - name: Install mysql-apt-config 19 | apt: 20 | deb: https://dev.mysql.com/get/mysql-apt-config_0.8.16-1_all.deb 21 | register: task_result 22 | until: task_result is succeeded 23 | when: 24 | - mysql_daemon == 'mysqld' 25 | 26 | - name: Add Mysql apt key 27 | apt_key: 28 | keyserver: '{{ mysql_apt_keyserver }}' 29 | id: '{{ mysql_apt_key_id }}' 30 | state: present 31 | register: apt_key_install 32 | until: apt_key_install is succeeded 33 | 34 | - name: Copy mysql.list 35 | template: 36 | src: '{{ mysql_apt_template }}.j2' 37 | dest: /etc/apt/sources.list.d/mysql.list 38 | owner: root 39 | group: root 40 | mode: 0644 41 | become: true 42 | -------------------------------------------------------------------------------- /tasks/setup-RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add repository and install packages 3 | block: 4 | 5 | - name: Set {{ mysql_daemon }} {{ mysql_version }} repository file 6 | template: 7 | src: yum.repo.j2 8 | dest: '{{ mysql_repofile }}' 9 | owner: root 10 | group: root 11 | mode: 0644 12 | 13 | - name: Add gpgkey 14 | rpm_key: 15 | state: present 16 | key: '{{ mysql_gpgkey }}' 17 | fingerprint: '{{ mysql_fingerprint | default(omit) }}' 18 | register: task_result 19 | retries: 5 20 | delay: 2 21 | until: task_result is succeeded 22 | 23 | - name: Install requirements 24 | yum: 25 | name: '{{ mysql_python_packages }}' 26 | state: present 27 | update_cache: true 28 | register: task_result 29 | until: task_result is succeeded 30 | 31 | - name: Disable mysql dnf module if required 32 | ini_file: 33 | path: '/etc/dnf/modules.d/{{ module_item }}.module' 34 | section: '{{ module_item }}' 35 | option: state 36 | value: disabled 37 | no_extra_spaces: true 38 | mode: 0644 39 | loop: 40 | - mysql 41 | - mariadb 42 | loop_control: 43 | loop_var: module_item 44 | when: 45 | - ansible_distribution_major_version | int > 7 46 | 47 | become: true 48 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.6/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: ec2 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-aws-centos7-mysql-5.6 12 | platform: centos7 13 | instance_type: m5.large 14 | region: us-east-1 15 | vpc_subnet_id: subnet-05a2ef2b767afec50 16 | assign_public_ip: false 17 | spot_price: 0.05 18 | security_group_name: 19 | - default 20 | wait_timeout: 1800 21 | ssh_user: centos 22 | groups: 23 | - rhel_family 24 | - rhel7 25 | 26 | - name: test-aws-Debian9-mysql-5.6 27 | platform: debian9 28 | instance_type: m5.large 29 | region: us-east-1 30 | vpc_subnet_id: subnet-05a2ef2b767afec50 31 | assign_public_ip: false 32 | spot_price: 0.05 33 | security_group_name: 34 | - default 35 | wait_timeout: 1800 36 | ssh_user: admin 37 | groups: 38 | - debian_family 39 | - pip3 40 | 41 | provisioner: 42 | name: ansible 43 | log: false 44 | config_options: 45 | defaults: 46 | callback_whitelist: profile_tasks,timer 47 | inventory: 48 | group_vars: 49 | pip3: 50 | ansible_python_interpreter: /usr/bin/python3 51 | playbooks: 52 | create: ../resources/provisioning/AWS/create.yml 53 | destroy: ../resources/provisioning/AWS/destroy.yml 54 | verify: ../resources/tests/verify.yml 55 | prepare: ../resources/prepare.yml 56 | scenario: 57 | name: cloud-aws-direct-5.6 58 | verifier: 59 | name: ansible 60 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Run all tests 3 | # default: 4 | # image: 5 | # name: $AWS_ECR_REPO/docker-ansible-ci:ansible-2.9 6 | 7 | stages: 8 | - lint 9 | - deployment test 10 | 11 | before_script: 12 | - rm -rf molecule/resources/provisioning 13 | - ansible --version 14 | - ansible-lint --version 15 | - molecule --version 16 | - git clone https://github.com/lean-delivery/ansible-molecule-drivers.git molecule/resources/provisioning 17 | 18 | variables: 19 | GET_SOURCES_ATTEMPTS: "5" 20 | 21 | Lint: 22 | stage: lint 23 | before_script: 24 | - git clone https://github.com/lean-delivery/ansible-lint-rules.git ~/ansible-lint-rules 25 | script: 26 | - yamllint . -c .yamllint 27 | - ansible-lint . -c .ansible-lint 28 | after_script: 29 | - rm -rf ~/ansible-lint-rules 30 | tags: 31 | - lint 32 | 33 | .Docker mysql: 34 | stage: deployment test 35 | script: 36 | - molecule test -s default 37 | tags: 38 | - aws 39 | 40 | AWS mysql 5.7 ansible-2.10: 41 | image: 42 | name: $AWS_ECR_REPO/docker-ansible-ci:ansible-2.10 43 | variables: 44 | AWS_REGION: us-east-1 45 | stage: deployment test 46 | script: 47 | - molecule test -s cloud-aws-direct-5.7 48 | tags: 49 | - aws 50 | 51 | AWS mysql 5.6: 52 | variables: 53 | AWS_REGION: us-east-1 54 | stage: deployment test 55 | script: 56 | - molecule test -s cloud-aws-direct-5.6 57 | tags: 58 | - aws 59 | 60 | AWS mysql 5.5: 61 | variables: 62 | AWS_REGION: us-east-1 63 | stage: deployment test 64 | script: 65 | - molecule test -s cloud-aws-direct-5.5 66 | tags: 67 | - aws 68 | 69 | AWS mysql-8.0 ansible-2.10: 70 | image: 71 | name: $AWS_ECR_REPO/docker-ansible-ci:ansible-2.10 72 | variables: 73 | AWS_REGION: us-east-1 74 | stage: deployment test 75 | script: 76 | - molecule test -s cloud-aws-direct-8.0 77 | tags: 78 | - aws 79 | 80 | AWS mariadb ansible 2.10: 81 | image: 82 | name: $AWS_ECR_REPO/docker-ansible-ci:ansible-2.10 83 | variables: 84 | AWS_REGION: us-east-1 85 | stage: deployment test 86 | script: 87 | - molecule test -s cloud-aws-direct-mariadb 88 | tags: 89 | - aws 90 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 13 | 14 | ##### SUMMARY 15 | 16 | 17 | ##### ISSUE TYPE 18 | - Bug Report 19 | 20 | ##### COMPONENT NAME 21 | 23 | 24 | ##### ANSIBLE VERSION 25 | 26 | ``` 27 | 28 | ``` 29 | 30 | ##### CONFIGURATION 31 | 34 | 35 | ##### OS / ENVIRONMENT 36 | 40 | 41 | ##### STEPS TO REPRODUCE 42 | 44 | 45 | 46 | ```yaml 47 | 48 | ``` 49 | 50 | 51 | 52 | ##### EXPECTED RESULTS 53 | 54 | 55 | ##### ACTUAL RESULTS 56 | 57 | 58 | 59 | ``` 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 13 | 14 | ##### SUMMARY 15 | 16 | 17 | ##### ISSUE TYPE 18 | - Feature Idea 19 | 20 | ##### COMPONENT NAME 21 | 23 | 24 | ##### ANSIBLE VERSION 25 | 26 | ``` 27 | 28 | ``` 29 | 30 | ##### CONFIGURATION 31 | 34 | 35 | ##### OS / ENVIRONMENT 36 | 40 | 41 | ##### STEPS TO REPRODUCE 42 | 44 | 45 | 46 | ```yaml 47 | 48 | ``` 49 | 50 | 51 | 52 | ##### EXPECTED RESULTS 53 | 54 | 55 | ##### ACTUAL RESULTS 56 | 57 | 58 | 59 | ``` 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📝 Documentation Report 3 | about: Ask us about docs 4 | 5 | --- 6 | 7 | 13 | 14 | ##### SUMMARY 15 | 16 | 17 | ##### ISSUE TYPE 18 | - Documentation Report 19 | 20 | ##### COMPONENT NAME 21 | 23 | 24 | ##### ANSIBLE VERSION 25 | 26 | ``` 27 | 28 | ``` 29 | 30 | ##### CONFIGURATION 31 | 34 | 35 | ##### OS / ENVIRONMENT 36 | 40 | 41 | ##### STEPS TO REPRODUCE 42 | 44 | 45 | 46 | ```yaml 47 | 48 | ``` 49 | 50 | 51 | 52 | ##### EXPECTED RESULTS 53 | 54 | 55 | ##### ACTUAL RESULTS 56 | 57 | 58 | 59 | ``` 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-5.7/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: ec2 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-aws-centos7-mysql-5.7 12 | platform: centos7 13 | instance_type: m5.large 14 | region: us-east-1 15 | vpc_subnet_id: subnet-05a2ef2b767afec50 16 | assign_public_ip: false 17 | spot_price: 0.05 18 | security_group_name: 19 | - default 20 | wait_timeout: 1800 21 | ssh_user: centos 22 | groups: 23 | - rhel_family 24 | - rhel7 25 | 26 | - name: test-aws-ubuntu18-mysql-5.7.31 27 | platform: ubuntu18 28 | instance_type: m5.large 29 | region: us-east-1 30 | vpc_subnet_id: subnet-05a2ef2b767afec50 31 | assign_public_ip: false 32 | security_group_name: 33 | - default 34 | spot_price: 0.05 35 | wait_timeout: 1800 36 | ssh_user: ubuntu 37 | groups: 38 | - debian_family 39 | - minor_ubuntu 40 | 41 | - name: test-aws-Debian9-mysql-5.7 42 | platform: debian9 43 | instance_type: m5.large 44 | region: us-east-1 45 | vpc_subnet_id: subnet-05a2ef2b767afec50 46 | assign_public_ip: false 47 | spot_price: 0.05 48 | security_group_name: 49 | - default 50 | wait_timeout: 1800 51 | ssh_user: admin 52 | groups: 53 | - debian_family 54 | - pip3 55 | 56 | provisioner: 57 | name: ansible 58 | log: false 59 | config_options: 60 | defaults: 61 | callback_whitelist: profile_tasks,timer 62 | inventory: 63 | group_vars: 64 | pip3: 65 | ansible_python_interpreter: /usr/bin/python3 66 | minor_ubuntu: 67 | ansible_python_interpreter: /usr/bin/python3 68 | mysql_artifacts: 69 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client_5.7.31-1ubuntu18.04_amd64.deb 70 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-client_5.7.31-1ubuntu18.04_amd64.deb 71 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-community-server_5.7.31-1ubuntu18.04_amd64.deb 72 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-server_5.7.31-1ubuntu18.04_amd64.deb 73 | playbooks: 74 | create: ../resources/provisioning/AWS/create.yml 75 | destroy: ../resources/provisioning/AWS/destroy.yml 76 | verify: ../resources/tests/verify.yml 77 | prepare: ../resources/prepare.yml 78 | scenario: 79 | name: cloud-aws-direct-5.7 80 | verifier: 81 | name: ansible 82 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-8.0/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: ec2 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-aws-centos7-mysql-8.0 12 | platform: centos7 13 | instance_type: m5.large 14 | region: us-east-1 15 | vpc_subnet_id: subnet-05a2ef2b767afec50 16 | assign_public_ip: false 17 | spot_price: 0.05 18 | security_group_name: 19 | - default 20 | wait_timeout: 1800 21 | ssh_user: centos 22 | groups: 23 | - rhel_family 24 | - rhel7 25 | 26 | - name: test-aws-centos8-mysql-8.0 27 | platform: centos8 28 | instance_type: m5.large 29 | region: us-east-1 30 | vpc_subnet_id: subnet-05a2ef2b767afec50 31 | assign_public_ip: false 32 | spot_price: 0.05 33 | security_group_name: 34 | - default 35 | wait_timeout: 1800 36 | ssh_user: centos 37 | groups: 38 | - rhel_family 39 | 40 | - name: test-aws-ubuntu18-mysql-8.0 41 | platform: ubuntu18 42 | instance_type: m5.large 43 | region: us-east-1 44 | vpc_subnet_id: subnet-05a2ef2b767afec50 45 | assign_public_ip: false 46 | security_group_name: 47 | - default 48 | spot_price: 0.05 49 | wait_timeout: 1800 50 | ssh_user: ubuntu 51 | groups: 52 | - debian_family 53 | 54 | - name: test-aws-Debian9-mysql-8.0 55 | platform: debian9 56 | instance_type: m5.large 57 | region: us-east-1 58 | vpc_subnet_id: subnet-05a2ef2b767afec50 59 | assign_public_ip: false 60 | spot_price: 0.05 61 | security_group_name: 62 | - default 63 | wait_timeout: 1800 64 | ssh_user: admin 65 | groups: 66 | - debian_family 67 | - pip3 68 | 69 | - name: test-aws-Debian10-mysql-8.0 70 | platform: debian10 71 | instance_type: m5.large 72 | region: us-east-1 73 | vpc_subnet_id: subnet-05a2ef2b767afec50 74 | assign_public_ip: false 75 | spot_price: 0.05 76 | security_group_name: 77 | - default 78 | wait_timeout: 1800 79 | ssh_user: admin 80 | groups: 81 | - debian_family 82 | - pip3 83 | 84 | provisioner: 85 | name: ansible 86 | log: false 87 | config_options: 88 | defaults: 89 | callback_whitelist: profile_tasks,timer 90 | inventory: 91 | group_vars: 92 | pip3: 93 | ansible_python_interpreter: /usr/bin/python3 94 | playbooks: 95 | create: ../resources/provisioning/AWS/create.yml 96 | destroy: ../resources/provisioning/AWS/destroy.yml 97 | verify: ../resources/tests/verify.yml 98 | prepare: ../resources/prepare.yml 99 | lint: 100 | name: ansible-lint 101 | scenario: 102 | name: cloud-aws-direct-8.0 103 | verifier: 104 | name: ansible 105 | lint: 106 | name: ansible-lint 107 | -------------------------------------------------------------------------------- /molecule/cloud-aws-direct-mariadb/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | collections-path: ~/.ansible/collections 7 | driver: 8 | name: ec2 9 | lint: ansible-lint 10 | platforms: 11 | - name: test-aws-centos7-mariadb 12 | platform: centos7 13 | instance_type: m5.large 14 | region: us-east-1 15 | vpc_subnet_id: subnet-05a2ef2b767afec50 16 | assign_public_ip: false 17 | spot_price: 0.05 18 | security_group_name: 19 | - default 20 | wait_timeout: 1800 21 | ssh_user: centos 22 | groups: 23 | - rhel_family 24 | - rhel7 25 | 26 | - name: test-aws-centos8-mariadb 27 | platform: centos8 28 | instance_type: m5.large 29 | region: us-east-1 30 | vpc_subnet_id: subnet-05a2ef2b767afec50 31 | assign_public_ip: false 32 | spot_price: 0.05 33 | security_group_name: 34 | - default 35 | wait_timeout: 1800 36 | ssh_user: centos 37 | groups: 38 | - rhel_family 39 | - disablerepo 40 | 41 | - name: test-aws-ubuntu18-mariadb 42 | platform: ubuntu18 43 | instance_type: m5.large 44 | region: us-east-1 45 | vpc_subnet_id: subnet-05a2ef2b767afec50 46 | assign_public_ip: false 47 | security_group_name: 48 | - default 49 | spot_price: 0.05 50 | wait_timeout: 1800 51 | ssh_user: ubuntu 52 | groups: 53 | - debian_family 54 | 55 | - name: test-aws-Debian9-mariadb 56 | platform: debian9 57 | instance_type: m5.large 58 | region: us-east-1 59 | vpc_subnet_id: subnet-05a2ef2b767afec50 60 | assign_public_ip: false 61 | spot_price: 0.05 62 | security_group_name: 63 | - default 64 | wait_timeout: 1800 65 | ssh_user: admin 66 | groups: 67 | - debian_family 68 | - pip3 69 | 70 | - name: test-aws-Debian10-mariadb 71 | platform: debian10 72 | instance_type: m5.large 73 | region: us-east-1 74 | vpc_subnet_id: subnet-05a2ef2b767afec50 75 | assign_public_ip: false 76 | spot_price: 0.05 77 | security_group_name: 78 | - default 79 | wait_timeout: 1800 80 | ssh_user: admin 81 | groups: 82 | - debian_family 83 | - pip3 84 | 85 | provisioner: 86 | name: ansible 87 | log: false 88 | config_options: 89 | defaults: 90 | callback_whitelist: profile_tasks,timer 91 | inventory: 92 | group_vars: 93 | pip3: 94 | ansible_python_interpreter: /usr/bin/python3 95 | playbooks: 96 | create: ../resources/provisioning/AWS/create.yml 97 | destroy: ../resources/provisioning/AWS/destroy.yml 98 | verify: ../resources/tests/verify.yml 99 | prepare: ../resources/prepare.yml 100 | lint: 101 | name: ansible-lint 102 | scenario: 103 | name: cloud-aws-direct-mariadb 104 | verifier: 105 | name: ansible 106 | lint: 107 | name: ansible-lint 108 | -------------------------------------------------------------------------------- /tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup config and start MySQL service 3 | block: 4 | 5 | - name: Pip update 6 | pip: 7 | name: 8 | - pip 9 | state: present 10 | extra_args: --upgrade 11 | executable: pip3 12 | register: pip_install 13 | until: pip_install is succeeded 14 | vars: 15 | ansible_python_interpreter: /usr/bin/python3 16 | 17 | - name: Install {{ mysql_daemon }} {{ mysql_version }} 18 | package: 19 | name: '{{ (mysql_artifacts | default(false,true)) | ternary(omit, mysql_packages) }}' 20 | deb: '{{ mysql_artifact_item | default(omit, true) }}' 21 | state: present 22 | update_cache: true 23 | register: task_result 24 | until: task_result is succeeded 25 | loop: >- 26 | {{ mysql_artifacts | default([''], true) }} 27 | loop_control: 28 | loop_var: mysql_artifact_item 29 | 30 | - name: Check mysql version 31 | command: mysql --version 32 | register: mysql_out_version 33 | changed_when: false 34 | 35 | - name: Install mysqlclient 36 | pip: 37 | name: 38 | - mysqlclient 39 | state: present 40 | executable: pip3 41 | register: mysql_connector_install 42 | until: mysql_connector_install is succeeded 43 | 44 | - name: Extract mysql version 45 | set_fact: 46 | mysql_version: >- 47 | {{ mysql_out_version.stdout | regex_findall('(\d+\.\d+\.\d+)') | first }} 48 | 49 | - name: debug mysql_version 50 | debug: 51 | var: mysql_version 52 | 53 | - name: Copy my.cnf global MySQL configuration 54 | template: 55 | src: my.cnf.j2 56 | dest: '{{ mysql_config_file }}' 57 | owner: root 58 | group: root 59 | mode: 0644 60 | force: '{{ overwrite_global_mycnf }}' 61 | register: mysql_config 62 | notify: restart mysql 63 | 64 | - name: Verify mysql pid directory exists 65 | file: 66 | path: '{{ mysql_pid_file | dirname }}' 67 | state: directory 68 | owner: mysql 69 | group: mysql 70 | mode: 0755 71 | 72 | - name: Clear InnoDB logs 73 | include_tasks: clear_innodb_logs.yml 74 | when: mysql_config.changed | bool 75 | 76 | - name: Verify mysql include directory exists 77 | file: 78 | path: '{{ mysql_config_include_dir }}' 79 | state: directory 80 | owner: root 81 | group: root 82 | mode: 0755 83 | when: mysql_config_include_files | length 84 | 85 | - name: Create slow query log file (if configured) 86 | file: 87 | path: '{{ mysql_slow_query_log_file }}' 88 | state: touch 89 | owner: mysql 90 | group: '{{ mysql_log_file_group }}' 91 | mode: 0640 92 | register: touch_slowlog 93 | changed_when: touch_slowlog.diff.before.state != 'file' 94 | when: mysql_slow_query_log_enabled 95 | 96 | - name: Create errorlog file (if configured) 97 | file: 98 | path: '{{ mysql_log_error }}' 99 | state: touch 100 | owner: mysql 101 | group: '{{ mysql_log_file_group }}' 102 | mode: 0640 103 | seuser: system_u 104 | serole: object_r 105 | setype: mysqld_log_t 106 | selevel: s0 107 | register: touch_log 108 | changed_when: touch_log.diff.before.state != 'file' 109 | 110 | - name: Start MySQL service 111 | service: 112 | name: '{{ mysql_service_name }}' 113 | state: started 114 | enabled: true 115 | 116 | become: true 117 | 118 | - name: Wait for service to be ready 119 | wait_for: 120 | port: '{{ mysql_port }}' 121 | host: '{{ mysql_bind_address }}' 122 | connect_timeout: 3 123 | delay: 3 124 | timeout: 30 125 | -------------------------------------------------------------------------------- /tasks/secure-installation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Reset default passwort for root didn't work with module mysql_user, 3 | # that why we use this block 4 | - name: Make new mysql admin user 5 | block: 6 | - name: Check mysql version 7 | mysql_variables: 8 | variable: version 9 | login_user: '{{ mysql_root_username }}' 10 | login_password: '{{ mysql_root_password }}' 11 | register: mysql_version_out 12 | become: true 13 | 14 | - name: Out mysql_version 15 | debug: 16 | var: mysql_version_out 17 | 18 | rescue: 19 | - name: Clear InnoDB logs and restart MySQL in skip-grant-tables mode 20 | block: 21 | - name: Clear InnoDB logs 22 | include_tasks: clear_innodb_logs.yml 23 | 24 | - name: Copy init.cnf for skip-grant-tables mode 25 | template: 26 | src: init.cnf.j2 27 | dest: '{{ mysql_config_file }}' 28 | owner: root 29 | group: root 30 | mode: 0644 31 | force: true 32 | 33 | - name: Start mysql with skip-grant-tables 34 | service: 35 | name: '{{ mysql_service_name }}' 36 | state: started 37 | become: true 38 | 39 | - name: Check mysql version 40 | mysql_variables: 41 | variable: version 42 | register: mysql_version_out 43 | until: mysql_version_out is succeeded 44 | become: true 45 | 46 | - name: Copy reset.sql for root reset 47 | template: 48 | src: reset.sql.j2 49 | dest: /tmp/reset.sql 50 | force: true 51 | mode: 0644 52 | 53 | - name: Reset root credentials 54 | mysql_db: 55 | name: mysql 56 | state: import 57 | target: /tmp/reset.sql 58 | login_unix_socket: '{{ mysql_socket }}' 59 | become: true 60 | 61 | - name: Clear InnoDB logs and restart MySQL in normal mode 62 | block: 63 | - name: Clear InnoDB logs 64 | include_tasks: clear_innodb_logs.yml 65 | 66 | - name: Copy my.cnf global MySQL configuration 67 | template: 68 | src: my.cnf.j2 69 | dest: '{{ mysql_config_file }}' 70 | mode: 0644 71 | owner: root 72 | group: root 73 | 74 | - name: Start mysql 75 | service: 76 | name: '{{ mysql_service_name }}' 77 | state: started 78 | become: true 79 | 80 | - name: Remove file reset.sql 81 | file: 82 | path: /tmp/reset.sql 83 | state: absent 84 | 85 | - name: Wait for service to be ready 86 | wait_for: 87 | port: '{{ mysql_port }}' 88 | host: '{{ mysql_bind_address }}' 89 | connect_timeout: 3 90 | delay: 3 91 | timeout: 30 92 | 93 | - name: Setup admin credentials 94 | block: 95 | - name: Ensure default user is present 96 | mysql_user: 97 | name: '{{ mysql_user_name }}' 98 | host: localhost 99 | password: '{{ mysql_user_password }}' 100 | priv: '*.*:ALL,GRANT' 101 | state: present 102 | login_user: '{{ mysql_root_username }}' 103 | login_password: '{{ mysql_root_password }}' 104 | login_unix_socket: '{{ mysql_socket }}' 105 | when: mysql_user_name != 'root' 106 | 107 | - name: Copy .my.cnf file with password credentials 108 | template: 109 | src: user-my.cnf.j2 110 | dest: '{{ mysql_user_home }}/.my.cnf' 111 | owner: '{{ mysql_user_name }}' 112 | mode: 0600 113 | when: mysql_user_name != mysql_root_username 114 | 115 | - name: Update mysql root password for all root accounts 116 | mysql_user: 117 | name: root 118 | host_all: true 119 | password: '{{ mysql_root_password }}' 120 | check_implicit_admin: true 121 | priv: '*.*:ALL,GRANT' 122 | login_user: '{{ mysql_root_username }}' 123 | login_password: '{{ mysql_root_password }}' 124 | login_unix_socket: '{{ mysql_socket }}' 125 | no_log: true 126 | tags: 127 | - molecule-idempotence-notest 128 | 129 | - name: Remove anonymous MySQL users 130 | mysql_user: 131 | name: '' 132 | host_all: true 133 | state: absent 134 | login_user: '{{ mysql_root_username }}' 135 | login_password: '{{ mysql_root_password }}' 136 | 137 | - name: Remove MySQL test database 138 | mysql_db: 139 | name: test 140 | state: absent 141 | login_user: '{{ mysql_root_username }}' 142 | login_password: '{{ mysql_root_password }}' 143 | become: true 144 | -------------------------------------------------------------------------------- /templates/my.cnf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [client] 4 | #password = your_password 5 | port = {{ mysql_port }} 6 | socket = {{ mysql_socket }} 7 | 8 | [mysqld] 9 | {% if mysql_version is version('5.7.2', '>=') and mysql_daemon in ['mysqld'] %} 10 | default_authentication_plugin = mysql_native_password 11 | {% endif %} 12 | user = mysql 13 | port = {{ mysql_port }} 14 | bind-address = {{ mysql_bind_address }} 15 | datadir = {{ mysql_datadir }} 16 | socket = {{ mysql_socket }} 17 | pid-file = {{ mysql_pid_file }} 18 | {% if mysql_skip_name_resolve %} 19 | skip-name-resolve 20 | {% endif %} 21 | {% if mysql_sql_mode %} 22 | sql_mode = {{ mysql_sql_mode }} 23 | {% endif %} 24 | 25 | # Logging configuration. 26 | {% if mysql_log_error == 'syslog' or mysql_log == 'syslog' %} 27 | syslog 28 | syslog-tag = {{ mysql_syslog_tag }} 29 | {% else %} 30 | {% if mysql_log %} 31 | general-log = 1 32 | general_log_file = {{ mysql_log }} 33 | {% endif %} 34 | log-error = {{ mysql_log_error }} 35 | {% endif %} 36 | 37 | {% if mysql_slow_query_log_enabled %} 38 | # Slow query log configuration. 39 | slow_query_log = 1 40 | slow_query_log_file = {{ mysql_slow_query_log_file }} 41 | long_query_time = {{ mysql_slow_query_time }} 42 | {% endif %} 43 | 44 | {% if mysql_replication_master %} 45 | # Replication 46 | server-id = {{ mysql_server_id }} 47 | 48 | {% if mysql_replication_role == 'master' %} 49 | log_bin = mysql-bin 50 | log-bin-index = mysql-bin.index 51 | expire_logs_days = {{ mysql_expire_logs_days }} 52 | max_binlog_size = {{ mysql_max_binlog_size }} 53 | binlog_format = {{mysql_binlog_format}} 54 | 55 | {% for db in mysql_databases %} 56 | {% if db.replicate|default(1) %} 57 | binlog_do_db = {{ db.name }} 58 | {% else %} 59 | binlog_ignore_db = {{ db.name }} 60 | {% endif %} 61 | {% endfor %} 62 | {% endif %} 63 | 64 | {% if mysql_replication_role == 'slave' %} 65 | read_only 66 | relay-log = relay-bin 67 | relay-log-index = relay-bin.index 68 | {% endif %} 69 | {% endif %} 70 | 71 | # Disabling symbolic-links is recommended to prevent assorted security risks 72 | symbolic-links = 0 73 | 74 | # http://dev.mysql.com/doc/refman/5.5/en/performance-schema.html 75 | ;performance_schema 76 | 77 | # Memory settings. 78 | key_buffer_size = {{ mysql_key_buffer_size }} 79 | max_allowed_packet = {{ mysql_max_allowed_packet }} 80 | table_open_cache = {{ mysql_table_open_cache }} 81 | sort_buffer_size = {{ mysql_sort_buffer_size }} 82 | read_buffer_size = {{ mysql_read_buffer_size }} 83 | read_rnd_buffer_size = {{ mysql_read_rnd_buffer_size }} 84 | myisam_sort_buffer_size = {{ mysql_myisam_sort_buffer_size }} 85 | thread_cache_size = {{ mysql_thread_cache_size }} 86 | {% if mysql_version is version('8.0', '<') %} 87 | query_cache_type = {{ mysql_query_cache_type }} 88 | query_cache_size = {{ mysql_query_cache_size }} 89 | query_cache_limit = {{ mysql_query_cache_limit }} 90 | {% endif %} 91 | max_connections = {{ mysql_max_connections }} 92 | tmp_table_size = {{ mysql_tmp_table_size }} 93 | max_heap_table_size = {{ mysql_max_heap_table_size }} 94 | group_concat_max_len = {{ mysql_group_concat_max_len }} 95 | join_buffer_size = {{ mysql_join_buffer_size }} 96 | 97 | # Other settings. 98 | wait_timeout = {{ mysql_wait_timeout }} 99 | lower_case_table_names = {{ mysql_lower_case_table_names }} 100 | event_scheduler = {{ mysql_event_scheduler_state }} 101 | 102 | # InnoDB settings. 103 | {% if mysql_supports_innodb_large_prefix and (mysql_version is version('8.0', '<')) %} 104 | innodb_large_prefix = {{ mysql_innodb_large_prefix }} 105 | innodb_file_format = {{ mysql_innodb_file_format }} 106 | {% endif %} 107 | innodb_file_per_table = {{ mysql_innodb_file_per_table }} 108 | innodb_buffer_pool_size = {{ mysql_innodb_buffer_pool_size }} 109 | innodb_log_file_size = {{ mysql_innodb_log_file_size }} 110 | innodb_log_buffer_size = {{ mysql_innodb_log_buffer_size }} 111 | innodb_flush_log_at_trx_commit = {{ mysql_innodb_flush_log_at_trx_commit }} 112 | innodb_lock_wait_timeout = {{ mysql_innodb_lock_wait_timeout }} 113 | 114 | # Additional parameters 115 | {% if additional_parameters is defined %} 116 | {% for param in additional_parameters %} 117 | {{ param.name }} = {{ param.value }} 118 | {% endfor %} 119 | {% endif %} 120 | 121 | [mysqldump] 122 | quick 123 | max_allowed_packet = {{ mysql_mysqldump_max_allowed_packet }} 124 | 125 | [mysqld_safe] 126 | pid-file = {{ mysql_pid_file }} 127 | 128 | {% if mysql_config_include_files | length %} 129 | # * IMPORTANT: Additional settings that can override those from this file! 130 | # The files must end with '.cnf', otherwise they'll be ignored. 131 | # 132 | !includedir {{ mysql_config_include_dir }} 133 | {% endif %} 134 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Define a what install mysqd(and which version 5.5/5.6/5.7) or mariabd (version 10.1) 3 | mysql_daemon: '{{ mysql_default_daemon }}' 4 | mysql_version: '{{ mysql_default_version }}' 5 | mysql_repo: '{{ mysql_default_repo }}' 6 | mysql_repofile: '{{ mysql_default_repofile }}' 7 | mysql_gpgkey: '{{ mysql_default_gpgkey }}' 8 | mysql_apt_keyserver: '{{ mysql_default_apt_keyserver }}' 9 | mysql_apt_key_id: '{{ mysql_default_apt_key_id }}' 10 | mysql_repo_package: '{{ mysql_default_repo_package }}' 11 | 12 | # Define a custom list of packages to install; if none provided, the default 13 | # package list from vars/[OS-family].yml will be used. 14 | mysql_packages: '{{ mysql_default_packages }}' 15 | # 16 | mysql_python_packages: '{{ mysql_default_python_packages }}' 17 | 18 | # Set this to the user ansible is logging in as - should have root 19 | # or sudo access 20 | mysql_user_home: /root 21 | mysql_user_name: root 22 | mysql_user_password: root 23 | 24 | # The default root user installed by mysql - almost always root 25 | mysql_root_home: /root 26 | mysql_root_username: root 27 | mysql_root_password: root 28 | 29 | # Whether my.cnf should be updated on every run. 30 | overwrite_global_mycnf: true 31 | 32 | # The following variables have a default value depending on operating system. The default 33 | # parametr from vars/[OS-family].yml will be used 34 | mysql_config_file: '{{ mysql_default_config_file }}' 35 | mysql_config_include_dir: '{{ mysql_default_config_include_dir }}' 36 | 37 | # MySQL connection settings. 38 | mysql_port: "3306" 39 | mysql_bind_address: "0.0.0.0" 40 | mysql_skip_name_resolve: false 41 | mysql_datadir: '{{ mysql_default_datadir }}' 42 | mysql_sql_mode: "" 43 | # The following variables have a default value depending on operating system. The default 44 | # parametr from vars/[OS-family].yml will be used 45 | mysql_pid_file: '{{ mysql_default_pid_file }}' 46 | mysql_socket: '{{ mysql_default_socket }}' 47 | 48 | mysql_service_name: "{{ mysql_default_service_name }}" 49 | 50 | # Log file settings. 51 | mysql_log_file_group: mysql 52 | 53 | # Slow query log settings. 54 | mysql_slow_query_log_enabled: false 55 | mysql_slow_query_time: "2" 56 | # The following variable has a default value depending on operating system. 57 | mysql_slow_query_log_file: '{{ mysql_default_slow_query_log_file }}' 58 | 59 | # Memory settings (default values optimized ~512MB RAM). 60 | mysql_key_buffer_size: "256M" 61 | mysql_max_allowed_packet: "64M" 62 | mysql_table_open_cache: "256" 63 | mysql_sort_buffer_size: "1M" 64 | mysql_read_buffer_size: "1M" 65 | mysql_read_rnd_buffer_size: "4M" 66 | mysql_myisam_sort_buffer_size: "64M" 67 | mysql_thread_cache_size: "8" 68 | mysql_query_cache_type: "0" 69 | mysql_query_cache_size: "16M" 70 | mysql_query_cache_limit: "1M" 71 | mysql_max_connections: "151" 72 | mysql_tmp_table_size: "16M" 73 | mysql_max_heap_table_size: "16M" 74 | mysql_group_concat_max_len: "1024" 75 | mysql_join_buffer_size: "262144" 76 | 77 | # Other settings. 78 | mysql_lower_case_table_names: 0 79 | mysql_wait_timeout: 28800 80 | mysql_event_scheduler_state: DISABLED 81 | 82 | mysql_supports_innodb_large_prefix: '{{ mysql_default_supports_innodb_large_prefix }}' 83 | # InnoDB settings. 84 | mysql_innodb_file_per_table: 1 85 | # Set .._buffer_pool_size up to 80% of RAM but beware of setting too high. 86 | mysql_innodb_buffer_pool_size: 256M 87 | # Set .._log_file_size to 25% of buffer pool size. 88 | mysql_innodb_log_file_size: 64M 89 | mysql_innodb_log_buffer_size: 8M 90 | mysql_innodb_flush_log_at_trx_commit: 1 91 | mysql_innodb_lock_wait_timeout: 50 92 | 93 | # These settings require MySQL > 5.5. 94 | mysql_innodb_large_prefix: 1 95 | mysql_innodb_file_format: barracuda 96 | 97 | # mysqldump settings. 98 | mysql_mysqldump_max_allowed_packet: 64M 99 | 100 | # Logging settings. 101 | mysql_log: "/var/log/{{ mysql_daemon }}.log" 102 | # The following variables have a default value depending on operating system. The default 103 | # parametr from vars/[OS-family].yml will be used 104 | mysql_log_error: '{{ mysql_default_log_error }}' 105 | mysql_syslog_tag: '{{ mysql_default_syslog_tag }}' 106 | 107 | mysql_config_include_files: [] 108 | # - src: path/relative/to/playbook/file.cnf 109 | # - { src: path/relative/to/playbook/anotherfile.cnf, force: yes } 110 | 111 | # Databases. 112 | mysql_databases: [] 113 | # - name: example 114 | # collation: utf8_general_ci 115 | # encoding: utf8 116 | # replicate: 1 117 | 118 | # Users. 119 | mysql_users: [] 120 | # - name: example 121 | # host: 127.0.0.1 122 | # password: secret 123 | # priv: *.*:USAGE 124 | 125 | # Replication settings (replication is only enabled if master/user have values). 126 | mysql_server_id: "1" 127 | mysql_max_binlog_size: "100M" 128 | mysql_binlog_format: "ROW" 129 | mysql_expire_logs_days: "10" 130 | mysql_replication_role: "" 131 | mysql_replication_master: "" 132 | # Same keys as `mysql_users` above. 133 | mysql_replication_user: [] 134 | 135 | # if you want to add some other parametrs, which must be in my.cnf file. 136 | # additional_parameters: [] 137 | # - name: some_parametr 138 | # value: 11 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: MySQL 2 | [![License](https://img.shields.io/badge/license-Apache-green.svg?style=flat)](https://raw.githubusercontent.com/lean-delivery/ansible-role-mysql/master/LICENSE) 3 | [![Build status](https://gitlab.com/lean-delivery/ansible-role-mysql/badges/master/pipeline.svg)](https://gitlab.com/lean-delivery/ansible-role-mysql/-/commits/master) 4 | [![Galaxy](https://img.shields.io/badge/galaxy-lean__delivery.mysql-blue.svg)](https://galaxy.ansible.com/lean_delivery/mysql) 5 | ![Ansible](https://img.shields.io/ansible/role/d/35413.svg) 6 | ![Ansible](https://img.shields.io/badge/dynamic/json.svg?label=min_ansible_version&url=https%3A%2F%2Fgalaxy.ansible.com%2Fapi%2Fv1%2Froles%2F35413%2F&query=$.min_ansible_version) 7 | 8 | ## Summary 9 | 10 | This role installs and configures MySQL or MariaDB server on RHEL/CentOS servers. 11 | 12 | ## Role tasks 13 | 14 | - Installs MySQL/MariaDB 15 | - Reset root password for mysql 16 | - Create db and users 17 | 18 | ## Requirements 19 | 20 | - Supported versions: 21 | - Oracle Mysql 22 | - 5.5 23 | - 5.6 24 | - 5.7 25 | - 8.0 26 | - Mariadb 27 | - 10.3 28 | - 10.4 29 | - 10.5 30 | - Supported OS: 31 | - RHEL 32 | - 7 33 | - 8 34 | - CentOS 35 | - 7 36 | - 8 37 | - Ubuntu 38 | - 18.04 39 | - Debian 40 | - 9 41 | - 10 42 | 43 | ## Role Variables 44 | 45 | Available variables are listed below, along with default values: 46 | 47 | Mysql/MariaDB repository settings: 48 | 49 | mysql_repo: *default value depends on OS* 50 | mysql_gpgkey: *default value depends on OS* 51 | mysql_apt_keyserver: *default value depends on OS* 52 | mysql_repofile: /etc/yum.repos.d/mysql.repo|/etc/yum.repos.d/mariadb.repo 53 | mysql_apt_key_id: *default value depends on OS* 54 | mysql_repo_disable_list: *default - undefined*. For CentOS 8 it's now list of `AppStream` and `Stream-AppStream`. 55 | 56 | mysql_packages: 57 | - mysql-community-server # (mysql-community-server/MariaDB-server) 58 | - mysql-community-client # (mysql-community-client/MariaDB-client) 59 | 60 | If you want to select a specific minor version of package, you can enter appropriate package name, for instance: 61 | 62 | mysql_packages: 63 | - mysql-community-server-8.0.16-2.el7.x86_64 64 | - mysql-community-client-8.0.16-2.el7.x86_64 65 | 66 | Alternatively, you can define the packages as a list of external urls by setting the variable `mysql_artifacts`, e.g.: 67 | 68 | mysql_artifacts: 69 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client_5.7.31-1ubuntu18.04_amd64.deb 70 | - https://downloads.mysql.com/archives/get/p/23/file/mysql-community-server_5.7.31-1ubuntu18.04_amd64.deb 71 | 72 | ### NOTE: *This option was tested only for deb-based packages at the moment.* 73 | # (MariaDB-common) 74 | mysql_daemon: mysqld # (mysqld/mariadb) 75 | mysql_version: 5.7 # (for mysql = 5.5/5.6/5.7; for mariadb = last (10.5) ) 76 | 77 | (OS-specific, RedHat/CentOS defaults listed here) Packages to be installed. In some situations, you may need to add additional packages, like `mysql-devel`. 78 | 79 | mysql_user_home: /root 80 | mysql_user_name: root 81 | mysql_user_password: root 82 | 83 | The home directory inside which Python MySQL settings will be stored, which Ansible will use when connecting to MySQL. This should be the home directory of the user which runs this Ansible role. The `mysql_user_name` and `mysql_user_password` can be set if you are running this role under a non-root user account and want to set a non-root user. 84 | 85 | mysql_root_home: /root 86 | mysql_root_username: root 87 | mysql_root_password: root 88 | 89 | The MySQL root user account details. 90 | 91 | mysql_config_file: *default value depends on OS* 92 | mysql_config_include_dir: *default value depends on OS* 93 | 94 | The main my.cnf configuration file and include directory. 95 | 96 | overwrite_global_mycnf: true 97 | 98 | Whether the global my.cnf should be overwritten each time this role is run. Setting this to `no` tells Ansible to only create the `my.cnf` file if it doesn't exist. This should be left at its default value (`yes`) if you'd like to use this role's variables to configure MySQL. 99 | 100 | mysql_config_include_files: [] 101 | 102 | A list of files that should override the default global my.cnf. Each item in the array requires a "src" parameter which is a path to a file. An optional "force" parameter can force the file to be updated each time ansible runs. 103 | 104 | mysql_databases: [] 105 | 106 | The MySQL databases to create. A database has the values `name`, `encoding` (defaults to `utf8`), `collation` (defaults to `utf8_general_ci`) and `replicate` (defaults to `1`, only used if replication is configured). The formats of these are the same as in the `mysql_db` module. 107 | 108 | mysql_users: [] 109 | 110 | The MySQL users and their privileges. A user has the values: 111 | 112 | - `name` 113 | - `host` (defaults to `localhost`) 114 | - `password` (can be plaintext or encrypted—if encrypted, set `encrypted: yes`) 115 | - `encrypted` (defaults to `no`) 116 | - `priv` (defaults to `*.*:USAGE`) 117 | - `append_privs` (defaults to `no`) 118 | - `state` (defaults to `present`) 119 | 120 | The formats of these are the same as in the `mysql_user` module. 121 | 122 | mysql_port: "3306" 123 | mysql_bind_address: '0.0.0.0' 124 | mysql_datadir: /var/lib/mysql 125 | mysql_socket: *default value depends on OS* 126 | mysql_pid_file: *default value depends on OS* 127 | 128 | Default MySQL connection configuration. 129 | 130 | mysql_log_file_group: mysql *adm on Debian* 131 | mysql_log: "" 132 | mysql_log_error: *default value depends on OS* 133 | mysql_syslog_tag: *default value depends on OS* 134 | 135 | MySQL logging configuration. Setting `mysql_log` (the general query log) or `mysql_log_error` to `syslog` will make MySQL log to syslog using the `mysql_syslog_tag`. 136 | 137 | mysql_slow_query_log_enabled: false 138 | mysql_slow_query_log_file: *default value depends on OS* 139 | mysql_slow_query_time: 2 140 | 141 | Slow query log settings. Note that the log file will be created by this role, but if you're running on a server with SELinux or AppArmor, you may need to add this path to the allowed paths for MySQL, or disable the mysql profile. For example, on Debian/Ubuntu, you can run `sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/usr.sbin.mysqld && sudo service apparmor restart`. 142 | 143 | mysql_key_buffer_size: "256M" 144 | mysql_max_allowed_packet: "64M" 145 | mysql_table_open_cache: "256" 146 | [...] 147 | 148 | The rest of the settings in `defaults/main.yml` control MySQL's memory usage and some other common settings. The default values are tuned for a server where MySQL can consume ~512 MB RAM, so you should consider adjusting them to suit your particular server better. 149 | 150 | mysql_server_id: "1" 151 | mysql_max_binlog_size: "100M" 152 | mysql_binlog_format: "ROW" 153 | mysql_expire_logs_days: "10" 154 | mysql_replication_role: '' 155 | mysql_replication_master: '' 156 | mysql_replication_user: [] 157 | 158 | Replication settings. Set `mysql_server_id` and `mysql_replication_role` by server (e.g. the master would be ID `1`, with the `mysql_replication_role` of `master`, and the slave would be ID `2`, with the `mysql_replication_role` of `slave`). The `mysql_replication_user` uses the same keys as `mysql_users`, and is created on master servers, and used to replicate on all the slaves. 159 | 160 | `mysql_replication_master` needs to resolve to an IP or a hostname which is accessable to the Slaves (this could be a `/etc/hosts` injection or some other means), otherwise the slaves cannot communicate to the master. 161 | 162 | ## additional_parameters 163 | Also you can set other parametrs, which are not listed here and it will be written to the configuration file `my.cnf`. 164 | 165 | Example: 166 | ```yaml 167 | additional_parameters: 168 | - name: mysql_expire_logs_days 169 | value: 11 170 | ``` 171 | # 172 | 173 | ### MariaDB usage 174 | 175 | This role works with either MySQL or a compatible version of MariaDB. On RHEL/CentOS 7+, the mariadb database engine was substituted as the default MySQL replacement package. No modifications are necessary though all of the variables still reference 'mysql' instead of mariadb. 176 | 177 | ## Dependencies 178 | 179 | Due to new breaking changes in MySQL 8.0 we included modified module `mysql_user`. It's shipping with that role and resides in `library` directory. Current Ansible module `mysql_user` is not compatible with latest changes but fixes are already in place and new Ansible release 2.8 should not require customized module to run. 180 | 181 | ## Example Playbooks 182 | 183 | ### Installing MySQL 5.7 version: 184 | ```yaml 185 | - hosts: db-servers 186 | roles: 187 | - role: lean_delivery.mysql 188 | vars: 189 | mysql_root_password: Super_P@s$0rd 190 | mysql_databases: 191 | - name: example2_db 192 | encoding: latin1 193 | collation: latin1_general_ci 194 | mysql_users: 195 | - name: example2_user 196 | host: "%" 197 | password: Sime32_U$er_p@ssw0rd 198 | priv: "example2_db.*:ALL" 199 | mysql_port: 3306 200 | mysql_bind_address: '0.0.0.0' 201 | mysql_daemon: mysqld 202 | mysql_version: 5.7 203 | ``` 204 | 205 | ### Installing MySQL 8.0 version: 206 | ```yaml 207 | - hosts: db-servers 208 | roles: 209 | - role: lean_delivery.mysql 210 | vars: 211 | mysql_root_password: 88TEM-veDRE<888serd 212 | mysql_databases: 213 | - name: example2_db 214 | encoding: latin1 215 | collation: latin1_general_ci 216 | mysql_users: 217 | - name: example2_user 218 | host: "%" 219 | password: Sime32-SRRR-password 220 | priv: "example2_db.*:ALL" 221 | mysql_port: 3306 222 | mysql_bind_address: '0.0.0.0' 223 | mysql_daemon: mysqld 224 | mysql_version: 8.0 225 | mysql_packages: 226 | - mysql-server 227 | ``` 228 | 229 | ### Installing MariaDB: 230 | ```yaml 231 | - hosts: db-servers 232 | roles: 233 | - role: lean_delivery.mysql 234 | vars: 235 | mysql_root_password: 88TEM-veDRE<888serd 236 | mysql_databases: 237 | - name: example2_db 238 | encoding: latin1 239 | collation: latin1_general_ci 240 | mysql_users: 241 | - name: example2_user 242 | host: "%" 243 | password: Sime32-SRRR-password 244 | priv: "example2_db.*:ALL" 245 | mysql_port: 3306 246 | mysql_bind_address: '0.0.0.0' 247 | mysql_daemon: mariadb 248 | ``` 249 | 250 | 251 | __Note__: CentOS always do password reset via `rescue` section: It should be noted that the play continues if a rescue section completes successfully as it ‘erases’ the error status (but not the reporting), this means it will appear in the **playbook statistics** ONLY. 252 | 253 | **ATTENTION!** Note that override parameters in playbook have to be set as `role parameters` (see example above). Parameters set as usual hostvars or inventory parameters will not supercede default role parameters set by role scenario depending on OS version etc. 254 | 255 | ## License 256 | Apache 257 | 258 | 259 | ## Author Information 260 | authors: 261 | 262 | - Lean Delivery Team team@lean-delivery.com 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 EPAM Systems 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /mysql_user.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2012, Mark Theunissen 5 | # Sponsored by Four Kitchens http://fourkitchens.com. 6 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 7 | 8 | from __future__ import absolute_import, division, print_function 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: mysql_user 14 | short_description: Adds or removes a user from a MySQL database 15 | description: 16 | - Adds or removes a user from a MySQL database. 17 | options: 18 | name: 19 | description: 20 | - Name of the user (role) to add or remove. 21 | type: str 22 | required: true 23 | password: 24 | description: 25 | - Set the user's password. 26 | type: str 27 | encrypted: 28 | description: 29 | - Indicate that the 'password' field is a `mysql_native_password` hash. 30 | type: bool 31 | default: no 32 | host: 33 | description: 34 | - The 'host' part of the MySQL username. 35 | type: str 36 | default: localhost 37 | host_all: 38 | description: 39 | - Override the host option, making ansible apply changes 40 | to all hostnames for a given user. 41 | - This option cannot be used when creating users. 42 | type: bool 43 | default: no 44 | priv: 45 | description: 46 | - "MySQL privileges string in the format: C(db.table:priv1,priv2)." 47 | - "Multiple privileges can be specified by separating each one using 48 | a forward slash: C(db.table:priv/db.table:priv)." 49 | - The format is based on MySQL C(GRANT) statement. 50 | - Database and table names can be quoted, MySQL-style. 51 | - If column privileges are used, the C(priv1,priv2) part must be 52 | exactly as returned by a C(SHOW GRANT) statement. If not followed, 53 | the module will always report changes. It includes grouping columns 54 | by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))). 55 | - Can be passed as a dictionary (see the examples). 56 | type: raw 57 | append_privs: 58 | description: 59 | - Append the privileges defined by priv to the existing ones for this 60 | user instead of overwriting existing ones. 61 | type: bool 62 | default: no 63 | tls_requires: 64 | description: 65 | - Set requirement for secure transport as a dictionary of requirements (see the examples). 66 | - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER. 67 | - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509. 68 | - U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls). 69 | type: dict 70 | version_added: 1.0.0 71 | sql_log_bin: 72 | description: 73 | - Whether binary logging should be enabled or disabled for the connection. 74 | type: bool 75 | default: yes 76 | state: 77 | description: 78 | - Whether the user should exist. 79 | - When C(absent), removes the user. 80 | type: str 81 | choices: [ absent, present ] 82 | default: present 83 | check_implicit_admin: 84 | description: 85 | - Check if mysql allows login as root/nopassword before trying supplied credentials. 86 | - If success, passed I(login_user)/I(login_password) will be ignored. 87 | type: bool 88 | default: no 89 | update_password: 90 | description: 91 | - C(always) will update passwords if they differ. 92 | - C(on_create) will only set the password for newly created users. 93 | type: str 94 | choices: [ always, on_create ] 95 | default: always 96 | plugin: 97 | description: 98 | - User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``). 99 | type: str 100 | version_added: '0.1.0' 101 | plugin_hash_string: 102 | description: 103 | - User's plugin hash string (``CREATE USER user IDENTIFIED WITH plugin AS plugin_hash_string``). 104 | type: str 105 | version_added: '0.1.0' 106 | plugin_auth_string: 107 | description: 108 | - User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``). 109 | type: str 110 | version_added: '0.1.0' 111 | resource_limits: 112 | description: 113 | - Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2. 114 | - "Available options are C(MAX_QUERIES_PER_HOUR: num), C(MAX_UPDATES_PER_HOUR: num), 115 | C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num)." 116 | - Used when I(state=present), ignored otherwise. 117 | type: dict 118 | version_added: '0.1.0' 119 | 120 | notes: 121 | - "MySQL server installs with default I(login_user) of C(root) and no password. 122 | To secure this user as part of an idempotent playbook, you must create at least two tasks: 123 | 1) change the root user's password, without providing any I(login_user)/I(login_password) details, 124 | 2) drop a C(~/.my.cnf) file containing the new root credentials. 125 | Subsequent runs of the playbook will then succeed by reading the new credentials from the file." 126 | - Currently, there is only support for the C(mysql_native_password) encrypted password hash module. 127 | - Supports (check_mode). 128 | 129 | seealso: 130 | - module: community.mysql.mysql_info 131 | - name: MySQL access control and account management reference 132 | description: Complete reference of the MySQL access control and account management documentation. 133 | link: https://dev.mysql.com/doc/refman/8.0/en/access-control.html 134 | - name: MySQL provided privileges reference 135 | description: Complete reference of the MySQL provided privileges documentation. 136 | link: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html 137 | 138 | author: 139 | - Jonathan Mainguy (@Jmainguy) 140 | - Benjamin Malynovytch (@bmalynovytch) 141 | - Lukasz Tomaszkiewicz (@tomaszkiewicz) 142 | extends_documentation_fragment: 143 | - community.mysql.mysql 144 | 145 | ''' 146 | 147 | EXAMPLES = r''' 148 | - name: Removes anonymous user account for localhost 149 | community.mysql.mysql_user: 150 | name: '' 151 | host: localhost 152 | state: absent 153 | 154 | - name: Removes all anonymous user accounts 155 | community.mysql.mysql_user: 156 | name: '' 157 | host_all: yes 158 | state: absent 159 | 160 | - name: Create database user with name 'bob' and password '12345' with all database privileges 161 | community.mysql.mysql_user: 162 | name: bob 163 | password: 12345 164 | priv: '*.*:ALL' 165 | state: present 166 | 167 | - name: Create database user using hashed password with all database privileges 168 | community.mysql.mysql_user: 169 | name: bob 170 | password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4' 171 | encrypted: yes 172 | priv: '*.*:ALL' 173 | state: present 174 | 175 | - name: Create database user with password and all database privileges and 'WITH GRANT OPTION' 176 | community.mysql.mysql_user: 177 | name: bob 178 | password: 12345 179 | priv: '*.*:ALL,GRANT' 180 | state: present 181 | 182 | - name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2 183 | community.mysql.mysql_user: 184 | state: present 185 | name: bob 186 | password: 12345dd 187 | priv: 188 | 'db1.*': 'ALL,GRANT' 189 | 'db2.*': 'ALL,GRANT' 190 | 191 | # Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. 192 | # Setting this privilege in this manner is supported for backwards compatibility only. 193 | # Use 'tls_requires' instead. 194 | - name: Modify user to require SSL connections 195 | community.mysql.mysql_user: 196 | name: bob 197 | append_privs: yes 198 | priv: '*.*:REQUIRESSL' 199 | state: present 200 | 201 | - name: Modify user to require TLS connection with a valid client certificate 202 | community.mysql.mysql_user: 203 | name: bob 204 | tls_requires: 205 | x509: 206 | state: present 207 | 208 | - name: Modify user to require TLS connection with a specific client certificate and cipher 209 | community.mysql.mysql_user: 210 | name: bob 211 | tls_requires: 212 | subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' 213 | cipher: 'ECDHE-ECDSA-AES256-SHA384' 214 | 215 | - name: Modify user to no longer require SSL 216 | community.mysql.mysql_user: 217 | name: bob 218 | tls_requires: 219 | 220 | - name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials 221 | community.mysql.mysql_user: 222 | login_user: root 223 | login_password: 123456 224 | name: sally 225 | state: absent 226 | 227 | # check_implicit_admin example 228 | - name: > 229 | Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. 230 | If mysql allows root/nopassword login, try it without the credentials first. 231 | If it's not allowed, pass the credentials 232 | community.mysql.mysql_user: 233 | check_implicit_admin: yes 234 | login_user: root 235 | login_password: 123456 236 | name: sally 237 | state: absent 238 | 239 | - name: Ensure no user named 'sally' exists at all 240 | community.mysql.mysql_user: 241 | name: sally 242 | host_all: yes 243 | state: absent 244 | 245 | - name: Specify grants composed of more than one word 246 | community.mysql.mysql_user: 247 | name: replication 248 | password: 12345 249 | priv: "*.*:REPLICATION CLIENT" 250 | state: present 251 | 252 | - name: Revoke all privileges for user 'bob' and password '12345' 253 | community.mysql.mysql_user: 254 | name: bob 255 | password: 12345 256 | priv: "*.*:USAGE" 257 | state: present 258 | 259 | # Example privileges string format 260 | # mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL 261 | 262 | - name: Example using login_unix_socket to connect to server 263 | community.mysql.mysql_user: 264 | name: root 265 | password: abc123 266 | login_unix_socket: /var/run/mysqld/mysqld.sock 267 | 268 | - name: Example of skipping binary logging while adding user 'bob' 269 | community.mysql.mysql_user: 270 | name: bob 271 | password: 12345 272 | priv: "*.*:USAGE" 273 | state: present 274 | sql_log_bin: no 275 | 276 | - name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin' 277 | community.mysql.mysql_user: 278 | name: bob 279 | plugin: AWSAuthenticationPlugin 280 | plugin_hash_string: RDS 281 | priv: '*.*:ALL' 282 | state: present 283 | 284 | - name: Limit bob's resources to 10 queries per hour and 5 connections per hour 285 | community.mysql.mysql_user: 286 | name: bob 287 | resource_limits: 288 | MAX_QUERIES_PER_HOUR: 10 289 | MAX_CONNECTIONS_PER_HOUR: 5 290 | 291 | # Example .my.cnf file for setting the root password 292 | # [client] 293 | # user=root 294 | # password=n<_665{vS43y 295 | ''' 296 | 297 | RETURN = '''#''' 298 | 299 | import re 300 | import string 301 | from distutils.version import LooseVersion 302 | 303 | from ansible.module_utils.basic import AnsibleModule 304 | from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError 305 | from ansible_collections.community.mysql.plugins.module_utils.mysql import ( 306 | mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec, get_server_version 307 | ) 308 | from ansible.module_utils.six import iteritems 309 | from ansible.module_utils._text import to_native 310 | 311 | 312 | VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', 313 | 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', 314 | 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', 315 | 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', 316 | 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', 317 | 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', 318 | 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', 319 | 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', 320 | 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', 'REQUIRESSL', 321 | 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', 322 | 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', 323 | 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', 324 | 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', 325 | 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', 326 | 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', 327 | 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', 328 | 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', 329 | 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', 330 | 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', 331 | 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', 332 | 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3', 333 | 'INVOKE LAMBDA', 334 | 'ALTER ROUTINE', 335 | 'BINLOG ADMIN', 336 | 'BINLOG MONITOR', 337 | 'BINLOG REPLAY', 338 | 'CONNECTION ADMIN', 339 | 'READ_ONLY ADMIN', 340 | 'REPLICATION MASTER ADMIN', 341 | 'REPLICATION SLAVE', 342 | 'REPLICATION SLAVE ADMIN', 343 | 'SET USER', 344 | 'SHOW_ROUTINE',)) 345 | 346 | 347 | class InvalidPrivsError(Exception): 348 | pass 349 | 350 | # =========================================== 351 | # MySQL module specific support methods. 352 | # 353 | 354 | 355 | # User Authentication Management changed in MySQL 5.7 and MariaDB 10.2.0 356 | def use_old_user_mgmt(cursor): 357 | cursor.execute("SELECT VERSION()") 358 | result = cursor.fetchone() 359 | version_str = result[0] 360 | version = version_str.split('.') 361 | 362 | if 'mariadb' in version_str.lower(): 363 | # Prior to MariaDB 10.2 364 | if int(version[0]) * 1000 + int(version[1]) < 10002: 365 | return True 366 | else: 367 | return False 368 | else: 369 | # Prior to MySQL 5.7 370 | if int(version[0]) * 1000 + int(version[1]) < 5007: 371 | return True 372 | else: 373 | return False 374 | 375 | 376 | def supports_identified_by_password(cursor): 377 | """ 378 | Determines whether the 'CREATE USER %s@%s IDENTIFIED BY PASSWORD %s' syntax is supported. This was dropped in 379 | MySQL 8.0. 380 | """ 381 | version_str = get_server_version(cursor) 382 | 383 | if 'mariadb' in version_str.lower(): 384 | return True 385 | else: 386 | return LooseVersion(version_str) < LooseVersion('8') 387 | 388 | 389 | def get_mode(cursor): 390 | cursor.execute('SELECT @@GLOBAL.sql_mode') 391 | result = cursor.fetchone() 392 | mode_str = result[0] 393 | if 'ANSI' in mode_str: 394 | mode = 'ANSI' 395 | else: 396 | mode = 'NOTANSI' 397 | return mode 398 | 399 | 400 | def user_exists(cursor, user, host, host_all): 401 | if host_all: 402 | cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s", (user,)) 403 | else: 404 | cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s", (user, host)) 405 | 406 | count = cursor.fetchone() 407 | return count[0] > 0 408 | 409 | 410 | def sanitize_requires(tls_requires): 411 | sanitized_requires = {} 412 | if tls_requires: 413 | for key in tls_requires.keys(): 414 | sanitized_requires[key.upper()] = tls_requires[key] 415 | if any([key in ["CIPHER", "ISSUER", "SUBJECT"] for key in sanitized_requires.keys()]): 416 | sanitized_requires.pop("SSL", None) 417 | sanitized_requires.pop("X509", None) 418 | return sanitized_requires 419 | 420 | if "X509" in sanitized_requires.keys(): 421 | sanitized_requires = "X509" 422 | else: 423 | sanitized_requires = "SSL" 424 | 425 | return sanitized_requires 426 | return None 427 | 428 | 429 | def mogrify_requires(query, params, tls_requires): 430 | if tls_requires: 431 | if isinstance(tls_requires, dict): 432 | k, v = zip(*tls_requires.items()) 433 | requires_query = " AND ".join(("%s %%s" % key for key in k)) 434 | params += v 435 | else: 436 | requires_query = tls_requires 437 | query = " REQUIRE ".join((query, requires_query)) 438 | return query, params 439 | 440 | 441 | def do_not_mogrify_requires(query, params, tls_requires): 442 | return query, params 443 | 444 | 445 | def get_tls_requires(cursor, user, host): 446 | if user: 447 | if not use_old_user_mgmt(cursor): 448 | query = "SHOW CREATE USER '%s'@'%s'" % (user, host) 449 | else: 450 | query = "SHOW GRANTS for '%s'@'%s'" % (user, host) 451 | 452 | cursor.execute(query) 453 | require_list = [tuple[0] for tuple in filter(lambda x: "REQUIRE" in x[0], cursor.fetchall())] 454 | require_line = require_list[0] if require_list else "" 455 | pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" 456 | requires_match = re.search(pattern, require_line) 457 | requires = requires_match.group().strip() if requires_match else "" 458 | if any((requires.startswith(req) for req in ('SSL', 'X509', 'NONE'))): 459 | requires = requires.split()[0] 460 | if requires == 'NONE': 461 | requires = None 462 | else: 463 | import shlex 464 | 465 | items = iter(shlex.split(requires)) 466 | requires = dict(zip(items, items)) 467 | return requires or None 468 | 469 | 470 | def get_grants(cursor, user, host): 471 | cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) 472 | grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0] 473 | pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))" 474 | grants = re.search(pattern, grants_line[0]).group().strip() 475 | return grants.split(", ") 476 | 477 | def privileges_get_all(cursor): 478 | privileges = [] 479 | cursor.execute('SELECT DISTINCT PRIVILEGE_TYPE FROM INFORMATION_SCHEMA.USER_PRIVILEGES WHERE IS_GRANTABLE="yes"') 480 | grants = cursor.fetchall() 481 | for grant in grants: 482 | privileges.append(grant[0]) 483 | return privileges 484 | 485 | def user_add(cursor, user, host, host_all, password, encrypted, 486 | plugin, plugin_hash_string, plugin_auth_string, new_priv, 487 | tls_requires, check_mode): 488 | # we cannot create users without a proper hostname 489 | if host_all: 490 | return False 491 | 492 | if check_mode: 493 | return True 494 | 495 | # Determine what user management method server uses 496 | old_user_mgmt = use_old_user_mgmt(cursor) 497 | 498 | mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires 499 | 500 | if password and encrypted: 501 | if supports_identified_by_password(cursor): 502 | query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password) 503 | else: 504 | query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, password) 505 | elif password and not encrypted: 506 | if old_user_mgmt: 507 | query_with_args = "CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password) 508 | else: 509 | cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) 510 | encrypted_password = cursor.fetchone()[0] 511 | query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password) 512 | elif plugin and plugin_hash_string: 513 | query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string) 514 | elif plugin and plugin_auth_string: 515 | query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) 516 | elif plugin: 517 | query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin) 518 | else: 519 | query_with_args = "CREATE USER %s@%s", (user, host) 520 | 521 | query_with_args_and_tls_requires = query_with_args + (tls_requires,) 522 | cursor.execute(*mogrify(*query_with_args_and_tls_requires)) 523 | 524 | if new_priv is not None: 525 | for db_table, priv in iteritems(new_priv): 526 | privileges_grant(cursor, user, host, db_table, priv, tls_requires) 527 | if tls_requires is not None: 528 | privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) 529 | return True 530 | 531 | 532 | def is_hash(password): 533 | ishash = False 534 | if len(password) == 41 and password[0] == '*': 535 | if frozenset(password[1:]).issubset(string.hexdigits): 536 | ishash = True 537 | return ishash 538 | 539 | 540 | def user_mod(cursor, user, host, host_all, password, encrypted, 541 | plugin, plugin_hash_string, plugin_auth_string, new_priv, 542 | append_privs, tls_requires, module): 543 | changed = False 544 | msg = "User unchanged" 545 | grant_option = False 546 | 547 | # Determine what user management method server uses 548 | old_user_mgmt = use_old_user_mgmt(cursor) 549 | 550 | if host_all: 551 | hostnames = user_get_hostnames(cursor, user) 552 | else: 553 | hostnames = [host] 554 | 555 | for host in hostnames: 556 | # Handle clear text and hashed passwords. 557 | if bool(password): 558 | 559 | # Get a list of valid columns in mysql.user table to check if Password and/or authentication_string exist 560 | cursor.execute(""" 561 | SELECT COLUMN_NAME FROM information_schema.COLUMNS 562 | WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') 563 | ORDER BY COLUMN_NAME DESC LIMIT 1 564 | """) 565 | colA = cursor.fetchone() 566 | 567 | cursor.execute(""" 568 | SELECT COLUMN_NAME FROM information_schema.COLUMNS 569 | WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') 570 | ORDER BY COLUMN_NAME ASC LIMIT 1 571 | """) 572 | colB = cursor.fetchone() 573 | 574 | # Select hash from either Password or authentication_string, depending which one exists and/or is filled 575 | cursor.execute(""" 576 | SELECT COALESCE( 577 | CASE WHEN %s = '' THEN NULL ELSE %s END, 578 | CASE WHEN %s = '' THEN NULL ELSE %s END 579 | ) 580 | FROM mysql.user WHERE user = %%s AND host = %%s 581 | """ % (colA[0], colA[0], colB[0], colB[0]), (user, host)) 582 | current_pass_hash = cursor.fetchone()[0] 583 | if isinstance(current_pass_hash, bytes): 584 | current_pass_hash = current_pass_hash.decode('ascii') 585 | 586 | if encrypted: 587 | encrypted_password = password 588 | if not is_hash(encrypted_password): 589 | module.fail_json(msg="encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))") 590 | else: 591 | if old_user_mgmt: 592 | cursor.execute("SELECT PASSWORD(%s)", (password,)) 593 | else: 594 | cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) 595 | encrypted_password = cursor.fetchone()[0] 596 | 597 | if current_pass_hash != encrypted_password: 598 | msg = "Password updated" 599 | if module.check_mode: 600 | return (True, msg) 601 | if old_user_mgmt: 602 | cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) 603 | msg = "Password updated (old style)" 604 | else: 605 | try: 606 | cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)) 607 | msg = "Password updated (new style)" 608 | except (mysql_driver.Error) as e: 609 | # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql 610 | # Replacing empty root password with new authentication mechanisms fails with error 1396 611 | if e.args[0] == 1396: 612 | cursor.execute( 613 | "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", 614 | ('mysql_native_password', encrypted_password, user, host) 615 | ) 616 | cursor.execute("FLUSH PRIVILEGES") 617 | msg = "Password forced update" 618 | else: 619 | raise e 620 | changed = True 621 | 622 | # Handle plugin authentication 623 | if plugin: 624 | cursor.execute("SELECT plugin, authentication_string FROM mysql.user " 625 | "WHERE user = %s AND host = %s", (user, host)) 626 | current_plugin = cursor.fetchone() 627 | 628 | update = False 629 | 630 | if current_plugin[0] != plugin: 631 | update = True 632 | 633 | if plugin_hash_string and current_plugin[1] != plugin_hash_string: 634 | update = True 635 | 636 | if plugin_auth_string and current_plugin[1] != plugin_auth_string: 637 | # this case can cause more updates than expected, 638 | # as plugin can hash auth_string in any way it wants 639 | # and there's no way to figure it out for 640 | # a check, so I prefer to update more often than never 641 | update = True 642 | 643 | if update: 644 | if plugin_hash_string: 645 | query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string) 646 | elif plugin_auth_string: 647 | query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) 648 | else: 649 | query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin) 650 | 651 | cursor.execute(*query_with_args) 652 | changed = True 653 | 654 | # Handle privileges 655 | if new_priv is not None: 656 | curr_priv = privileges_get(cursor, user, host) 657 | 658 | # If the user has privileges on a db.table that doesn't appear at all in 659 | # the new specification, then revoke all privileges on it. 660 | for db_table, priv in iteritems(curr_priv): 661 | # If the user has the GRANT OPTION on a db.table, revoke it first. 662 | if "GRANT" in priv: 663 | grant_option = True 664 | if db_table not in new_priv: 665 | if user != "root" and "PROXY" not in priv and not append_privs: 666 | msg = "Privileges updated" 667 | if module.check_mode: 668 | return (True, msg) 669 | privileges_revoke(cursor, user, host, db_table, priv, grant_option) 670 | changed = True 671 | 672 | # If the user doesn't currently have any privileges on a db.table, then 673 | # we can perform a straight grant operation. 674 | for db_table, priv in iteritems(new_priv): 675 | if db_table not in curr_priv: 676 | msg = "New privileges granted" 677 | if module.check_mode: 678 | return (True, msg) 679 | privileges_grant(cursor, user, host, db_table, priv, tls_requires) 680 | changed = True 681 | 682 | # If the db.table specification exists in both the user's current privileges 683 | # and in the new privileges, then we need to see if there's a difference. 684 | db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) 685 | for db_table in db_table_intersect: 686 | if "ALL" in new_priv[db_table]: 687 | if not append_privs: 688 | privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option) 689 | privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires) 690 | new_priv = {db_table: privileges_get_all(cursor)} 691 | if append_privs: 692 | priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) 693 | else: 694 | priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) 695 | 696 | if len(priv_diff) > 0: 697 | msg = "Privileges updated" 698 | if module.check_mode: 699 | return (True, msg) 700 | changed = True 701 | else: 702 | # If appending privileges, only the set difference between new privileges and current privileges matter. 703 | # The symmetric difference isn't relevant for append because existing privileges will not be revoked. 704 | if append_privs: 705 | priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) 706 | else: 707 | priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) 708 | 709 | if len(priv_diff) > 0: 710 | msg = "Privileges updated" 711 | if module.check_mode: 712 | return (True, msg) 713 | if not append_privs: 714 | privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option) 715 | privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires) 716 | changed = True 717 | 718 | # Handle TLS requirements 719 | current_requires = get_tls_requires(cursor, user, host) 720 | if current_requires != tls_requires: 721 | msg = "TLS requires updated" 722 | if module.check_mode: 723 | return (True, msg) 724 | if not old_user_mgmt: 725 | pre_query = "ALTER USER" 726 | else: 727 | pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host)) 728 | 729 | if tls_requires is not None: 730 | query = " ".join((pre_query, "%s@%s")) 731 | query_with_args = mogrify_requires(query, (user, host), tls_requires) 732 | else: 733 | query = " ".join((pre_query, "%s@%s REQUIRE NONE")) 734 | query_with_args = query, (user, host) 735 | 736 | cursor.execute(*query_with_args) 737 | changed = True 738 | 739 | return (changed, msg) 740 | 741 | 742 | def user_delete(cursor, user, host, host_all, check_mode): 743 | if check_mode: 744 | return True 745 | 746 | if host_all: 747 | hostnames = user_get_hostnames(cursor, user) 748 | else: 749 | hostnames = [host] 750 | 751 | for hostname in hostnames: 752 | cursor.execute("DROP USER %s@%s", (user, hostname)) 753 | 754 | return True 755 | 756 | 757 | def user_get_hostnames(cursor, user): 758 | cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", (user,)) 759 | hostnames_raw = cursor.fetchall() 760 | hostnames = [] 761 | 762 | for hostname_raw in hostnames_raw: 763 | hostnames.append(hostname_raw[0]) 764 | 765 | return hostnames 766 | 767 | 768 | def privileges_get(cursor, user, host): 769 | """ MySQL doesn't have a better method of getting privileges aside from the 770 | SHOW GRANTS query syntax, which requires us to then parse the returned string. 771 | Here's an example of the string that is returned from MySQL: 772 | 773 | GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass'; 774 | 775 | This function makes the query and returns a dictionary containing the results. 776 | The dictionary format is the same as that returned by privileges_unpack() below. 777 | """ 778 | output = {} 779 | cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) 780 | grants = cursor.fetchall() 781 | 782 | def pick(x): 783 | if x == 'ALL PRIVILEGES': 784 | return 'ALL' 785 | else: 786 | return x 787 | 788 | for grant in grants: 789 | res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0]) 790 | if res is None: 791 | raise InvalidPrivsError('unable to parse the MySQL grant string: %s' % grant[0]) 792 | privileges = res.group(1).split(",") 793 | privileges = [pick(x.strip()) for x in privileges] 794 | if "WITH GRANT OPTION" in res.group(7): 795 | privileges.append('GRANT') 796 | if 'REQUIRE SSL' in res.group(7): 797 | privileges.append('REQUIRESSL') 798 | db = res.group(2) 799 | output.setdefault(db, []).extend(privileges) 800 | return output 801 | 802 | 803 | def privileges_unpack(priv, mode): 804 | """ Take a privileges string, typically passed as a parameter, and unserialize 805 | it into a dictionary, the same format as privileges_get() above. We have this 806 | custom format to avoid using YAML/JSON strings inside YAML playbooks. Example 807 | of a privileges string: 808 | 809 | mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL 810 | 811 | The privilege USAGE stands for no privileges, so we add that in on *.* if it's 812 | not specified in the string, as MySQL will always provide this by default. 813 | """ 814 | if mode == 'ANSI': 815 | quote = '"' 816 | else: 817 | quote = '`' 818 | output = {} 819 | privs = [] 820 | for item in priv.strip().split('/'): 821 | pieces = item.strip().rsplit(':', 1) 822 | dbpriv = pieces[0].rsplit(".", 1) 823 | 824 | # Check for FUNCTION or PROCEDURE object types 825 | parts = dbpriv[0].split(" ", 1) 826 | object_type = '' 827 | if len(parts) > 1 and (parts[0] == 'FUNCTION' or parts[0] == 'PROCEDURE'): 828 | object_type = parts[0] + ' ' 829 | dbpriv[0] = parts[1] 830 | 831 | # Do not escape if privilege is for database or table, i.e. 832 | # neither quote *. nor .* 833 | for i, side in enumerate(dbpriv): 834 | if side.strip('`') != '*': 835 | dbpriv[i] = '%s%s%s' % (quote, side.strip('`'), quote) 836 | pieces[0] = object_type + '.'.join(dbpriv) 837 | 838 | if '(' in pieces[1]: 839 | output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper()) 840 | for i in output[pieces[0]]: 841 | privs.append(re.sub(r'\s*\(.*\)', '', i)) 842 | else: 843 | output[pieces[0]] = pieces[1].upper().split(',') 844 | privs = output[pieces[0]] 845 | new_privs = frozenset(privs) 846 | if not new_privs.issubset(VALID_PRIVS): 847 | raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS)) 848 | 849 | if '*.*' not in output: 850 | output['*.*'] = ['USAGE'] 851 | 852 | # if we are only specifying something like REQUIRESSL and/or GRANT (=WITH GRANT OPTION) in *.* 853 | # we still need to add USAGE as a privilege to avoid syntax errors 854 | if 'REQUIRESSL' in priv and not set(output['*.*']).difference(set(['GRANT', 'REQUIRESSL'])): 855 | output['*.*'].append('USAGE') 856 | 857 | return output 858 | 859 | 860 | def privileges_revoke(cursor, user, host, db_table, priv, grant_option): 861 | # Escape '%' since mysql db.execute() uses a format string 862 | db_table = db_table.replace('%', '%%') 863 | if grant_option: 864 | query = ["REVOKE GRANT OPTION ON %s" % db_table] 865 | query.append("FROM %s@%s") 866 | query = ' '.join(query) 867 | cursor.execute(query, (user, host)) 868 | priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')]) 869 | query = ["REVOKE %s ON %s" % (priv_string, db_table)] 870 | query.append("FROM %s@%s") 871 | query = ' '.join(query) 872 | cursor.execute(query, (user, host)) 873 | 874 | 875 | def privileges_grant(cursor, user, host, db_table, priv, tls_requires): 876 | # Escape '%' since mysql db.execute uses a format string and the 877 | # specification of db and table often use a % (SQL wildcard) 878 | db_table = db_table.replace('%', '%%') 879 | priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')]) 880 | query = ["GRANT %s ON %s" % (priv_string, db_table)] 881 | query.append("TO %s@%s") 882 | params = (user, host) 883 | if tls_requires and use_old_user_mgmt(cursor): 884 | query, params = mogrify_requires(" ".join(query), params, tls_requires) 885 | query = [query] 886 | if 'REQUIRESSL' in priv and not tls_requires: 887 | query.append("REQUIRE SSL") 888 | if 'GRANT' in priv: 889 | query.append("WITH GRANT OPTION") 890 | query = ' '.join(query) 891 | cursor.execute(query, params) 892 | 893 | 894 | def convert_priv_dict_to_str(priv): 895 | """Converts privs dictionary to string of certain format. 896 | 897 | Args: 898 | priv (dict): Dict of privileges that needs to be converted to string. 899 | 900 | Returns: 901 | priv (str): String representation of input argument. 902 | """ 903 | priv_list = ['%s:%s' % (key, val) for key, val in iteritems(priv)] 904 | 905 | return '/'.join(priv_list) 906 | 907 | 908 | # Alter user is supported since MySQL 5.6 and MariaDB 10.2.0 909 | def server_supports_alter_user(cursor): 910 | """Check if the server supports ALTER USER statement or doesn't. 911 | 912 | Args: 913 | cursor (cursor): DB driver cursor object. 914 | 915 | Returns: True if supports, False otherwise. 916 | """ 917 | cursor.execute("SELECT VERSION()") 918 | version_str = cursor.fetchone()[0] 919 | version = version_str.split('.') 920 | 921 | if 'mariadb' in version_str.lower(): 922 | # MariaDB 10.2 and later 923 | if int(version[0]) * 1000 + int(version[1]) >= 10002: 924 | return True 925 | else: 926 | return False 927 | else: 928 | # MySQL 5.6 and later 929 | if int(version[0]) * 1000 + int(version[1]) >= 5006: 930 | return True 931 | else: 932 | return False 933 | 934 | 935 | def get_resource_limits(cursor, user, host): 936 | """Get user resource limits. 937 | 938 | Args: 939 | cursor (cursor): DB driver cursor object. 940 | user (str): User name. 941 | host (str): User host name. 942 | 943 | Returns: Dictionary containing current resource limits. 944 | """ 945 | 946 | query = ('SELECT max_questions AS MAX_QUERIES_PER_HOUR, ' 947 | 'max_updates AS MAX_UPDATES_PER_HOUR, ' 948 | 'max_connections AS MAX_CONNECTIONS_PER_HOUR, ' 949 | 'max_user_connections AS MAX_USER_CONNECTIONS ' 950 | 'FROM mysql.user WHERE User = %s AND Host = %s') 951 | cursor.execute(query, (user, host)) 952 | res = cursor.fetchone() 953 | 954 | if not res: 955 | return None 956 | 957 | current_limits = { 958 | 'MAX_QUERIES_PER_HOUR': res[0], 959 | 'MAX_UPDATES_PER_HOUR': res[1], 960 | 'MAX_CONNECTIONS_PER_HOUR': res[2], 961 | 'MAX_USER_CONNECTIONS': res[3], 962 | } 963 | return current_limits 964 | 965 | 966 | def match_resource_limits(module, current, desired): 967 | """Check and match limits. 968 | 969 | Args: 970 | module (AnsibleModule): Ansible module object. 971 | current (dict): Dictionary with current limits. 972 | desired (dict): Dictionary with desired limits. 973 | 974 | Returns: Dictionary containing parameters that need to change. 975 | """ 976 | 977 | if not current: 978 | # It means the user does not exists, so we need 979 | # to set all limits after its creation 980 | return desired 981 | 982 | needs_to_change = {} 983 | 984 | for key, val in iteritems(desired): 985 | if key not in current: 986 | # Supported keys are listed in the documentation 987 | # and must be determined in the get_resource_limits function 988 | # (follow 'AS' keyword) 989 | module.fail_json(msg="resource_limits: key '%s' is unsupported." % key) 990 | 991 | try: 992 | val = int(val) 993 | except Exception: 994 | module.fail_json(msg="Can't convert value '%s' to integer." % val) 995 | 996 | if val != current.get(key): 997 | needs_to_change[key] = val 998 | 999 | return needs_to_change 1000 | 1001 | 1002 | def limit_resources(module, cursor, user, host, resource_limits, check_mode): 1003 | """Limit user resources. 1004 | 1005 | Args: 1006 | module (AnsibleModule): Ansible module object. 1007 | cursor (cursor): DB driver cursor object. 1008 | user (str): User name. 1009 | host (str): User host name. 1010 | resource_limit (dict): Dictionary with desired limits. 1011 | check_mode (bool): Run the function in check mode or not. 1012 | 1013 | Returns: True, if changed, False otherwise. 1014 | """ 1015 | if not server_supports_alter_user(cursor): 1016 | module.fail_json(msg="The server version does not match the requirements " 1017 | "for resource_limits parameter. See module's documentation.") 1018 | 1019 | current_limits = get_resource_limits(cursor, user, host) 1020 | 1021 | needs_to_change = match_resource_limits(module, current_limits, resource_limits) 1022 | 1023 | if not needs_to_change: 1024 | return False 1025 | 1026 | if needs_to_change and check_mode: 1027 | return True 1028 | 1029 | # If not check_mode 1030 | tmp = [] 1031 | for key, val in iteritems(needs_to_change): 1032 | tmp.append('%s %s' % (key, val)) 1033 | 1034 | query = "ALTER USER %s@%s" 1035 | query += ' WITH %s' % ' '.join(tmp) 1036 | cursor.execute(query, (user, host)) 1037 | return True 1038 | 1039 | 1040 | # =========================================== 1041 | # Module execution. 1042 | # 1043 | 1044 | 1045 | def main(): 1046 | argument_spec = mysql_common_argument_spec() 1047 | argument_spec.update( 1048 | user=dict(type='str', required=True, aliases=['name']), 1049 | password=dict(type='str', no_log=True), 1050 | encrypted=dict(type='bool', default=False), 1051 | host=dict(type='str', default='localhost'), 1052 | host_all=dict(type="bool", default=False), 1053 | state=dict(type='str', default='present', choices=['absent', 'present']), 1054 | priv=dict(type='raw'), 1055 | tls_requires=dict(type='dict'), 1056 | append_privs=dict(type='bool', default=False), 1057 | check_implicit_admin=dict(type='bool', default=False), 1058 | update_password=dict(type='str', default='always', choices=['always', 'on_create'], no_log=False), 1059 | sql_log_bin=dict(type='bool', default=True), 1060 | plugin=dict(default=None, type='str'), 1061 | plugin_hash_string=dict(default=None, type='str'), 1062 | plugin_auth_string=dict(default=None, type='str'), 1063 | resource_limits=dict(type='dict'), 1064 | ) 1065 | module = AnsibleModule( 1066 | argument_spec=argument_spec, 1067 | supports_check_mode=True, 1068 | ) 1069 | login_user = module.params["login_user"] 1070 | login_password = module.params["login_password"] 1071 | user = module.params["user"] 1072 | password = module.params["password"] 1073 | encrypted = module.boolean(module.params["encrypted"]) 1074 | host = module.params["host"].lower() 1075 | host_all = module.params["host_all"] 1076 | state = module.params["state"] 1077 | priv = module.params["priv"] 1078 | tls_requires = sanitize_requires(module.params["tls_requires"]) 1079 | check_implicit_admin = module.params["check_implicit_admin"] 1080 | connect_timeout = module.params["connect_timeout"] 1081 | config_file = module.params["config_file"] 1082 | append_privs = module.boolean(module.params["append_privs"]) 1083 | update_password = module.params['update_password'] 1084 | ssl_cert = module.params["client_cert"] 1085 | ssl_key = module.params["client_key"] 1086 | ssl_ca = module.params["ca_cert"] 1087 | check_hostname = module.params["check_hostname"] 1088 | db = '' 1089 | sql_log_bin = module.params["sql_log_bin"] 1090 | plugin = module.params["plugin"] 1091 | plugin_hash_string = module.params["plugin_hash_string"] 1092 | plugin_auth_string = module.params["plugin_auth_string"] 1093 | resource_limits = module.params["resource_limits"] 1094 | if priv and not isinstance(priv, (str, dict)): 1095 | module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv)) 1096 | 1097 | if priv and isinstance(priv, dict): 1098 | priv = convert_priv_dict_to_str(priv) 1099 | 1100 | if mysql_driver is None: 1101 | module.fail_json(msg=mysql_driver_fail_msg) 1102 | 1103 | cursor = None 1104 | try: 1105 | if check_implicit_admin: 1106 | try: 1107 | cursor, db_conn = mysql_connect(module, "root", "", config_file, ssl_cert, ssl_key, ssl_ca, db, 1108 | connect_timeout=connect_timeout, check_hostname=check_hostname) 1109 | except Exception: 1110 | pass 1111 | 1112 | if not cursor: 1113 | cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db, 1114 | connect_timeout=connect_timeout, check_hostname=check_hostname) 1115 | except Exception as e: 1116 | module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " 1117 | "Exception message: %s" % (config_file, to_native(e))) 1118 | 1119 | if not sql_log_bin: 1120 | cursor.execute("SET SQL_LOG_BIN=0;") 1121 | 1122 | if priv is not None: 1123 | try: 1124 | mode = get_mode(cursor) 1125 | except Exception as e: 1126 | module.fail_json(msg=to_native(e)) 1127 | try: 1128 | priv = privileges_unpack(priv, mode) 1129 | except Exception as e: 1130 | module.fail_json(msg="invalid privileges string: %s" % to_native(e)) 1131 | 1132 | if state == "present": 1133 | if user_exists(cursor, user, host, host_all): 1134 | try: 1135 | if update_password == "always": 1136 | changed, msg = user_mod(cursor, user, host, host_all, password, encrypted, 1137 | plugin, plugin_hash_string, plugin_auth_string, 1138 | priv, append_privs, tls_requires, module) 1139 | else: 1140 | changed, msg = user_mod(cursor, user, host, host_all, None, encrypted, 1141 | plugin, plugin_hash_string, plugin_auth_string, 1142 | priv, append_privs, tls_requires, module) 1143 | 1144 | except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: 1145 | module.fail_json(msg=to_native(e)) 1146 | else: 1147 | if host_all: 1148 | module.fail_json(msg="host_all parameter cannot be used when adding a user") 1149 | try: 1150 | changed = user_add(cursor, user, host, host_all, password, encrypted, 1151 | plugin, plugin_hash_string, plugin_auth_string, 1152 | priv, tls_requires, module.check_mode) 1153 | if changed: 1154 | msg = "User added" 1155 | 1156 | except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: 1157 | module.fail_json(msg=to_native(e)) 1158 | 1159 | if resource_limits: 1160 | changed = limit_resources(module, cursor, user, host, resource_limits, module.check_mode) or changed 1161 | 1162 | elif state == "absent": 1163 | if user_exists(cursor, user, host, host_all): 1164 | changed = user_delete(cursor, user, host, host_all, module.check_mode) 1165 | msg = "User deleted" 1166 | else: 1167 | changed = False 1168 | msg = "User doesn't exist" 1169 | module.exit_json(changed=changed, user=user, msg=msg) 1170 | 1171 | 1172 | if __name__ == '__main__': 1173 | main() 1174 | --------------------------------------------------------------------------------