diff options
Diffstat (limited to 'ansible_collections/community/mysql')
177 files changed, 22081 insertions, 0 deletions
diff --git a/ansible_collections/community/mysql/.github/patchback.yml b/ansible_collections/community/mysql/.github/patchback.yml new file mode 100644 index 000000000..33ad6e84a --- /dev/null +++ b/ansible_collections/community/mysql/.github/patchback.yml @@ -0,0 +1,5 @@ +--- +backport_branch_prefix: patchback/backports/ +backport_label_prefix: backport- +target_branch_prefix: stable- +... diff --git a/ansible_collections/community/mysql/.github/workflows/ansible-test-plugins.yml b/ansible_collections/community/mysql/.github/workflows/ansible-test-plugins.yml new file mode 100644 index 000000000..6533f9461 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/ansible-test-plugins.yml @@ -0,0 +1,363 @@ +--- +name: Plugins CI +on: + push: + paths: + - 'plugins/**' + - 'tests/**' + - '.github/workflows/ansible-test-plugins.yml' + pull_request: + paths: + - 'plugins/**' + - 'tests/**' + - '.github/workflows/ansible-test-plugins.yml' + schedule: + - cron: '0 6 * * *' + + +jobs: + sanity: + name: "Sanity (Ansible: ${{ matrix.ansible }})" + runs-on: ubuntu-20.04 + strategy: + matrix: + ansible: + - stable-2.12 + - stable-2.13 + - stable-2.14 + - devel + steps: + - name: Perform sanity testing + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: ${{ matrix.ansible }} + testing-type: sanity + pull-request-change-detection: true + + integration: + name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})" + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + ansible: + - stable-2.12 + - stable-2.13 + - stable-2.14 + - devel + db_engine_name: + - mysql + - mariadb + db_engine_version: + - 5.7.40 + - 8.0.31 + - 10.4.27 + - 10.5.18 + - 10.6.11 + python: + - '3.8' + - '3.9' + - '3.10' + connector_name: + - pymysql + - mysqlclient + connector_version: + - 0.7.11 + - 0.9.3 + - 1.0.2 + - 2.0.1 + - 2.0.3 + - 2.1.1 + exclude: + - db_engine_name: mysql + db_engine_version: 10.4.27 + + - db_engine_name: mysql + db_engine_version: 10.5.18 + + - db_engine_name: mysql + db_engine_version: 10.6.11 + + - db_engine_name: mariadb + db_engine_version: 5.7.40 + + - db_engine_name: mariadb + db_engine_version: 8.0.31 + + - connector_name: pymysql + connector_version: 2.0.1 + + - connector_name: pymysql + connector_version: 2.0.3 + + - connector_name: pymysql + connector_version: 2.1.1 + + - connector_name: mysqlclient + connector_version: 0.7.11 + + - connector_name: mysqlclient + connector_version: 0.9.3 + + - connector_name: mysqlclient + connector_version: 1.0.2 + + - db_engine_name: mariadb + connector_version: 0.7.11 + + - db_engine_version: 5.7.40 + python: '3.9' + + - db_engine_version: 5.7.40 + python: '3.10' + + - db_engine_version: 5.7.40 + ansible: stable-2.13 + + - db_engine_version: 5.7.40 + ansible: stable-2.14 + + - db_engine_version: 5.7.40 + ansible: devel + + - db_engine_version: 8.0.31 + python: '3.8' + + - db_engine_version: 8.0.31 + python: '3.8' + + - db_engine_version: 10.4.27 + python: '3.10' + + - db_engine_version: 10.4.27 + ansible: devel + + - db_engine_version: 10.6.11 + python: '3.8' + + - db_engine_version: 10.6.11 + python: '3.9' + + - python: '3.8' + connector_version: 1.0.2 + + - python: '3.8' + connector_version: 2.0.3 + + - python: '3.8' + connector_version: 2.1.1 + + - python: '3.9' + connector_version: 0.7.11 + + - python: '3.9' + connector_version: 1.0.2 + + - python: '3.9' + connector_version: 2.0.1 + + - python: '3.9' + connector_version: 2.1.1 + + - python: '3.10' + connector_version: 0.7.11 + + - python: '3.10' + connector_version: 0.9.3 + + - python: '3.10' + connector_version: 2.0.1 + + - python: '3.10' + connector_version: 2.0.3 + + - python: '3.8' + ansible: stable-2.13 + + - python: '3.8' + ansible: stable-2.14 + + - python: '3.8' + ansible: devel + + - python: '3.9' + ansible: stable-2.12 + + - python: '3.9' + ansible: devel + + - python: '3.10' + ansible: stable-2.12 + + services: + db_primary: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3307:3306 + # We write our own health-cmd because the mariadb container does not + # provide a healthcheck + options: >- + --health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + + db_replica1: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3308:3306 + options: >- + --health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + + db_replica2: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3309:3306 + options: >- + --health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + + steps: + + # No need to check for service health. GitHub Action took care of it. + + - name: Restart MySQL server with settings for replication + run: | + docker exec ${{ job.services.db_primary.id }} bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf' + docker exec ${{ job.services.db_replica1.id }} bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf' + docker exec ${{ job.services.db_replica2.id }} bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf' + docker restart -t 30 ${{ job.services.db_primary.id }} + docker restart -t 30 ${{ job.services.db_replica1.id }} + docker restart -t 30 ${{ job.services.db_replica2.id }} + + - name: Wait for the primary to be healthy + run: > + while ! /usr/bin/docker inspect + --format="{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}" + ${{ job.services.db_primary.id }} + | grep healthy && [[ "$SECONDS" -lt 120 ]]; do sleep 1; done + + - name: Compute docker_image - Set python_version_flat + run: > + echo "python_version_flat=$(echo ${{ matrix.python }} + | tr -d '.')" >> $GITHUB_ENV + + - name: Compute docker_image - Set connector_version_flat + run: > + echo "connector_version_flat=$(echo ${{ matrix.connector_version }} + |tr -d .)" >> $GITHUB_ENV + + - name: Compute docker_image - Set db_engine_version_flat + run: > + echo "db_engine_version_flat=$(echo ${{ matrix.db_engine_version }} + | awk -F '.' '{print $1 $2}')" >> $GITHUB_ENV + + - name: Compute docker_image - Set db_client + run: > + if [[ ${{ env.db_engine_version_flat }} == 57 ]]; then + echo "db_client=my57" >> $GITHUB_ENV; + else + echo "db_client=$(echo ${{ matrix.db_engine_name }})" >> $GITHUB_ENV; + fi + + - name: Set docker_image + run: > + docker_image_multiline=(" + ghcr.io/ansible-collections/community.mysql\ + /test-container-${{ env.db_client }}\ + -py${{ env.python_version_flat }}\ + -${{ matrix.connector_name }}${{ env.connector_version_flat }}\ + :latest") + + echo "docker_image=$(printf '%s' $docker_image_multiline)" + >> $GITHUB_ENV + + - name: >- + Perform integration testing against + Ansible version ${{ matrix.ansible }} + under Python ${{ matrix.python }} + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: ${{ matrix.ansible }} + pre-test-cmd: >- + echo Setting db_engine_name to "${{ matrix.db_engine_name }}"...; + echo -n "${{ matrix.db_engine_name }}" + > tests/integration/db_engine_name; + + echo Setting db_engine_version to \ + "${{ matrix.db_engine_version }}"...; + echo -n "${{ matrix.db_engine_version }}" + > tests/integration/db_engine_version; + + echo Setting Connector name to "${{ matrix.connector_name }}"...; + echo -n "${{ matrix.connector_name }}" + > tests/integration/connector_name; + + echo Setting Connector name to "${{ matrix.connector_version }}"...; + echo -n "${{ matrix.connector_version }}" + > tests/integration/connector_version; + + echo Setting Python version to "${{ matrix.python }}"...; + echo -n "${{ matrix.python }}" + > tests/integration/python; + + echo Setting Ansible version to "${{ matrix.ansible }}"...; + echo -n "${{ matrix.ansible }}" + > tests/integration/ansible + docker-image: ${{ env.docker_image }} + target-python-version: ${{ matrix.python }} + testing-type: integration + + units: + runs-on: ubuntu-20.04 + name: Units (â’¶${{ matrix.ansible }}) + strategy: + # As soon as the first unit test fails, + # cancel the others to free up the CI queue + fail-fast: true + matrix: + ansible: + - stable-2.12 + - stable-2.13 + - stable-2.14 + - devel + python: + - 3.8 + - 3.9 + exclude: + - python: '3.8' + ansible: stable-2.13 + - python: '3.8' + ansible: stable-2.14 + - python: '3.8' + ansible: devel + - python: '3.9' + ansible: stable-2.12 + + steps: + - name: >- + Perform unit testing against + Ansible version ${{ matrix.ansible }} + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: ${{ matrix.ansible }} + target-python-version: ${{ matrix.python }} + testing-type: units + pull-request-change-detection: true diff --git a/ansible_collections/community/mysql/.github/workflows/ansible-test-roles.yml b/ansible_collections/community/mysql/.github/workflows/ansible-test-roles.yml new file mode 100644 index 000000000..13e7d4178 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/ansible-test-roles.yml @@ -0,0 +1,74 @@ +--- +name: Roles CI +on: + push: + paths: + - 'roles/**' + - '.github/workflows/ansible-test-roles.yml' + pull_request: + paths: + - 'roles/**' + - '.github/workflows/ansible-test-roles.yml' + schedule: + - cron: '0 6 * * *' + +jobs: + molecule: + name: "Molecule (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, MySQL: ${{ matrix.mysql }})" + runs-on: ubuntu-20.04 + env: + PY_COLORS: 1 + ANSIBLE_FORCE_COLOR: 1 + strategy: + matrix: + mysql: + - 2.0.12 + ansible: + - stable-2.11 + - stable-2.12 + - stable-2.13 + - devel + python: + - 3.6 + - 3.8 + - 3.9 + exclude: + - python: 3.6 + ansible: stable-2.12 + - python: 3.6 + ansible: stable-2.13 + - python: 3.6 + ansible: devel + - python: 3.8 + ansible: stable-2.11 + - python: 3.8 + ansible: stable-2.13 + - python: 3.8 + ansible: devel + - python: 3.9 + ansible: stable-2.11 + - python: 3.9 + ansible: stable-2.12 + + steps: + + - name: Check out code + uses: actions/checkout@v2 + with: + path: ansible_collections/community/mysql + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Install ansible-core (${{ matrix.ansible }}) + run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + - name: Install molecule and related dependencies + run: | + pip install ansible-lint docker flake8 molecule testinfra yamllint + + # - name: Run molecule default test scenario + # run: for d in roles/*/; do (cd "$d" && molecule --version && molecule test) done + # working-directory: ./ansible_collections/community/mysql diff --git a/ansible_collections/community/mysql/.github/workflows/build-docker-image.yml b/ansible_collections/community/mysql/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..fa10268bb --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/build-docker-image.yml @@ -0,0 +1,67 @@ +--- +name: Build Docker Image for ansible-test + +on: + workflow_call: + inputs: + registry: + required: true + type: string + image_name: + required: true + type: string + context: + required: true + type: string + +jobs: + + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + # Requirement to use 'context' in docker/build-push-action@v3 + - name: Checkout repository + uses: actions/checkout@v3 + + # https://github.com/docker/login-action + - name: Log into registry ${{ inputs.registry }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.registry }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # https://github.com/docker/metadata-action + - name: Extract Docker metadata (tags, labels) + id: meta + uses: docker/metadata-action@v4 + with: + images: + "${{ inputs.registry }}\ + /${{ github.repository }}\ + /${{ inputs.image_name }}" + tags: latest + + # Setting up Docker Buildx with docker-container driver is required + # at the moment to be able to use a subdirectory with Git context + # + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # https://github.com/docker/build-push-action + - name: Build and push Docker image with Buildx + id: build-and-push + uses: docker/build-push-action@v3 + with: + context: ${{ inputs.context }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-mysqlclient211.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-mysqlclient211.yml new file mode 100644 index 000000000..be252b7b8 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-mysqlclient211.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py310-mysqlclient211 + +on: + push: + paths: + - 'test-containers/mariadb-py310-mysqlclient211/**' + - '.github/workflows/docker-image-mariadb-py310-mysqlclient211.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py310-mysqlclient211 + context: test-containers/mariadb-py310-mysqlclient211 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-pymysql102.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-pymysql102.yml new file mode 100644 index 000000000..90fec0ef8 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py310-pymysql102.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py310-pymysql102 + +on: + push: + paths: + - 'test-containers/mariadb-py310-pymysql102/**' + - '.github/workflows/docker-image-mariadb-py310-pymysql102.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py310-pymysql102 + context: test-containers/mariadb-py310-pymysql102 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-mysqlclient201.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-mysqlclient201.yml new file mode 100644 index 000000000..c9c04f4f9 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-mysqlclient201.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py38-mysqlclient201 + +on: + push: + paths: + - 'test-containers/mariadb-py38-mysqlclient201/**' + - '.github/workflows/docker-image-mariadb-py38-mysqlclient201.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py38-mysqlclient201 + context: test-containers/mariadb-py38-mysqlclient201 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-pymysql093.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-pymysql093.yml new file mode 100644 index 000000000..92d0a747a --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py38-pymysql093.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py38-pymysql093 + +on: + push: + paths: + - 'test-containers/mariadb-py38-pymysql093/**' + - '.github/workflows/docker-image-mariadb-py38-pymysql093.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py38-pymysql093 + context: test-containers/mariadb-py38-pymysql093 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-mysqlclient203.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-mysqlclient203.yml new file mode 100644 index 000000000..afad5affb --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-mysqlclient203.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py39-mysqlclient203 + +on: + push: + paths: + - 'test-containers/mariadb-py39-mysqlclient203/**' + - '.github/workflows/docker-image-mariadb-py39-mysqlclient203.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py39-mysqlclient203 + context: test-containers/mariadb-py39-mysqlclient203 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-pymysql093.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-pymysql093.yml new file mode 100644 index 000000000..1aa5a04e1 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mariadb-py39-pymysql093.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mariadb-py39-pymysql093 + +on: + push: + paths: + - 'test-containers/mariadb-py39-pymysql093/**' + - '.github/workflows/docker-image-mariadb-py39-pymysql093.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mariadb-py39-pymysql093 + context: test-containers/mariadb-py39-pymysql093 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-mysqlclient201.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-mysqlclient201.yml new file mode 100644 index 000000000..7aaf7e34a --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-mysqlclient201.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI my57-py38-mysqlclient201 + +on: + push: + paths: + - 'test-containers/my57-py38-mysqlclient201/**' + - '.github/workflows/docker-image-my57-py38-mysqlclient201.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-my57-py38-mysqlclient201 + context: test-containers/my57-py38-mysqlclient201 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql0711.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql0711.yml new file mode 100644 index 000000000..0bc2a9dd0 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql0711.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI my57-py38-pymysql0711 + +on: + push: + paths: + - 'test-containers/my57-py38-pymysql0711/**' + - '.github/workflows/docker-image-my57-py38-pymysql0711.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-my57-py38-pymysql0711 + context: test-containers/my57-py38-pymysql0711 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql093.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql093.yml new file mode 100644 index 000000000..462324bb9 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-my57-py38-pymysql093.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI my57-py38-pymysql093 + +on: + push: + paths: + - 'test-containers/my57-py38-pymysql093/**' + - '.github/workflows/docker-image-my57-py38-pymysql093.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-my57-py38-pymysql093 + context: test-containers/my57-py38-pymysql093 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-mysqlclient211.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-mysqlclient211.yml new file mode 100644 index 000000000..307aea763 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-mysqlclient211.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py310-mysqlclient211 + +on: + push: + paths: + - 'test-containers/mysql-py310-mysqlclient211/**' + - '.github/workflows/docker-image-mysql-py310-mysqlclient211.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py310-mysqlclient211 + context: test-containers/mysql-py310-mysqlclient211 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-pymysql102.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-pymysql102.yml new file mode 100644 index 000000000..6f7bf3f27 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py310-pymysql102.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py310-pymysql102 + +on: + push: + paths: + - 'test-containers/mysql-py310-pymysql102/**' + - '.github/workflows/docker-image-mysql-py310-pymysql102.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py310-pymysql102 + context: test-containers/mysql-py310-pymysql102 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-mysqlclient201.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-mysqlclient201.yml new file mode 100644 index 000000000..e0da5dfa9 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-mysqlclient201.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py38-mysqlclient201 + +on: + push: + paths: + - 'test-containers/mysql-py38-mysqlclient201/**' + - '.github/workflows/docker-image-mysql-py38-mysqlclient201.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py38-mysqlclient201 + context: test-containers/mysql-py38-mysqlclient201 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-pymysql093.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-pymysql093.yml new file mode 100644 index 000000000..3cc1e0aa9 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py38-pymysql093.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py38-pymysql093 + +on: + push: + paths: + - 'test-containers/mysql-py38-pymysql093/**' + - '.github/workflows/docker-image-mysql-py38-pymysql093.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py38-pymysql093 + context: test-containers/mysql-py38-pymysql093 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-mysqlclient203.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-mysqlclient203.yml new file mode 100644 index 000000000..0a3a256d9 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-mysqlclient203.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py39-mysqlclient203 + +on: + push: + paths: + - 'test-containers/mysql-py39-mysqlclient203/**' + - '.github/workflows/docker-image-mysql-py39-mysqlclient203.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py39-mysqlclient203 + context: test-containers/mysql-py39-mysqlclient203 diff --git a/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-pymysql093.yml b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-pymysql093.yml new file mode 100644 index 000000000..b974420a3 --- /dev/null +++ b/ansible_collections/community/mysql/.github/workflows/docker-image-mysql-py39-pymysql093.yml @@ -0,0 +1,21 @@ +--- +name: Docker Image CI mysql-py39-pymysql093 + +on: + push: + paths: + - 'test-containers/mysql-py39-pymysql093/*' + - '.github/workflows/docker-image-mysql-py39-pymysql093.yml' + - '.github/workflows/build-docker-image.yml' + branches-ignore: + - stable-* + +jobs: + + call-workflow-passing-data: + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + registry: ghcr.io + image_name: test-container-mysql-py39-pymysql093 + context: test-containers/mysql-py39-pymysql093 diff --git a/ansible_collections/community/mysql/.gitignore b/ansible_collections/community/mysql/.gitignore new file mode 100644 index 000000000..9555f5e60 --- /dev/null +++ b/ansible_collections/community/mysql/.gitignore @@ -0,0 +1,140 @@ +/tests/output/ +/tests/integration/inventory +/changelogs/.plugin-cache.yaml +*.swp + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# MacOS +.DS_Store + +# IntelliJ IDEA or PyCharm +.idea/ diff --git a/ansible_collections/community/mysql/CHANGELOG.rst b/ansible_collections/community/mysql/CHANGELOG.rst new file mode 100644 index 000000000..31ee41a87 --- /dev/null +++ b/ansible_collections/community/mysql/CHANGELOG.rst @@ -0,0 +1,295 @@ +======================================== +Community MySQL Collection Release Notes +======================================== + +.. contents:: Topics + +This changelog describes changes after version 2.0.0. + +v3.7.2 +====== + +Release Summary +--------------- + +This is a patch release of the community.mysql collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql module utils - use the connection arguments ``db`` instead of ``database`` and ``passwd`` instead of ``password`` when running with MySQLdb < 2.0.0 (https://github.com/ansible-collections/community.mysql/pull/553). + +v3.7.1 +====== + +Release Summary +--------------- + +This is a patch release of the community.mysql collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql module utils - use the connection arguments ``db`` instead of ``database`` and ``passwd`` instead of ``password`` when running with older mysql drivers (MySQLdb < 2.1.0 or PyMySQL < 1.0.0) (https://github.com/ansible-collections/community.mysql/pull/551). + +v3.7.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Minor Changes +------------- + +- mysql module utils - change deprecated connection parameters ``passwd`` and ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/177). +- mysql_user - add ``MAX_STATEMENT_TIME`` support for mariadb to the ``resource_limits`` argument (https://github.com/ansible-collections/community.mysql/issues/211). + +v3.6.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Minor Changes +------------- + +- mysql_info - add ``connector_name`` and ``connector_version`` to returned values (https://github.com/ansible-collections/community.mysql/pull/497). +- mysql_role - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). +- mysql_user - add plugin_auth_string as optional parameter to use a specific pam service if pam/auth_pam plugin is used (https://github.com/ansible-collections/community.mysql/pull/445). +- mysql_user - add the ``session_vars`` argument to set session variables at the beginning of module execution (https://github.com/ansible-collections/community.mysql/issues/478). +- mysql_user - display a more informative invalid privilege exception. Changes the exception handling of the granting permission logic to show the query executed , params and the exception message granting privileges fails` (https://github.com/ansible-collections/community.mysql/issues/465). +- mysql_user - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). +- setup_mysql - update MySQL tarball URL (https://github.com/ansible-collections/community.mysql/pull/491). + +Bugfixes +-------- + +- mysql_user - when revoke privs consists only of ``GRANT``, a 2nd revoke query is executed with empty privs to revoke that ended in an SQL exception (https://github.com/ansible-collections/community.mysql/pull/503). +- mysql_variables - add uppercase character pattern to regex to allow GLOBAL variables containing uppercase characters. This recognizes variable names used in Galera, for example, ``wsrep_OSU_method``, which breaks the normal pattern of all lowercase characters (https://github.com/ansible-collections/community.mysql/pull/501). + +v3.5.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql_user, mysql_role - mysql/mariadb recent versions translate 'ALL PRIVILEGES' to a list of specific privileges. That caused a change every time we modified user privileges. This fix compares privs before and after user modification to avoid this infinite change (https://github.com/ansible-collections/community.mysql/issues/77). + +v3.5.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.4.0. + +Minor Changes +------------- + +- mysql_replication - add a new option: ``primary_ssl_verify_server_cert`` (https://github.com//pull/435). + +Bugfixes +-------- + +- mysql_user - grant option was revoked accidentally when modifying users. This fix revokes grant option only when privs are setup to do that (https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807). + +v3.4.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.3.0. + +Major Changes +------------- + +- mysql_db - the ``pipefail`` argument's default value will be changed to ``true`` in community.mysql 4.0.0. If your target machines do not use ``bash`` as a default interpreter, set ``pipefail`` to ``false`` explicitly. However, we strongly recommend setting up ``bash`` as a default and ``pipefail=true`` as it will protect you from getting broken dumps you don't know about (https://github.com/ansible-collections/community.mysql/issues/407). + +Minor Changes +------------- + +- mysql_db - add the ``chdir`` argument to avoid failings when a dump file contains relative paths (https://github.com/ansible-collections/community.mysql/issues/395). +- mysql_db - add the ``pipefail`` argument to avoid broken dumps when ``state`` is ``dump`` and compression is used (https://github.com/ansible-collections/community.mysql/issues/256). + +Bugfixes +-------- + +- Include ``simplified_bsd.txt`` license file for various module utils. +- mysql_db - Using compression masks errors messages from mysql_dump. By default the fix is inactive to ensure retro-compatibility with system without bash. To activate the fix, use the module option ``pipefail=true`` (https://github.com/ansible-collections/community.mysql/issues/256). +- mysql_replication - when the ``primary_ssl`` argument is set to ``no``, the module will turn off SSL (https://github.com/ansible-collections/community.mysql/issues/393). + +v3.3.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.2.1. + +Minor Changes +------------- + +- mysql_role - add the argument ``members_must_exist`` (boolean, default true). The assertion that the users supplied in the ``members`` argument exist is only executed when the new argument ``members_must_exist`` is ``true``, to allow opt-out (https://github.com/ansible-collections/community.mysql/pull/369). +- mysql_user - Add the option ``on_new_username`` to argument ``update_password`` to reuse the password (plugin and authentication_string) when creating a new user if some user with the same name already exists. If the existing user with the same name have varying passwords, the password from the arguments is used like with ``update_password: always`` (https://github.com/ansible-collections/community.mysql/pull/365). +- mysql_user - Add the result field ``password_changed`` (boolean). It is true, when the user got a new password. When the user was created with ``update_password: on_new_username`` and an existing password was reused, ``password_changed`` is false (https://github.com/ansible-collections/community.mysql/pull/365). + +Bugfixes +-------- + +- mysql_query - fix false change reports when ``IF EXISTS/IF NOT EXISTS`` clause is used (https://github.com/ansible-collections/community.mysql/issues/268). +- mysql_role - don't add members to a role when creating the role and ``detach_members: true`` is set (https://github.com/ansible-collections/community.mysql/pull/367). +- mysql_role - in some cases (when "SHOW GRANTS" did not use backticks for quotes), no unwanted members were detached from the role (and redundant "GRANT" statements were executed for wanted members). This is fixed by querying the existing role members from the mysql.role_edges (MySQL) or mysql.roles_mapping (MariaDB) tables instead of parsing the "SHOW GRANTS" output (https://github.com/ansible-collections/community.mysql/pull/368). +- mysql_user - fix logic when ``update_password`` is set to ``on_create`` for users using ``plugin*`` arguments (https://github.com/ansible-collections/community.mysql/issues/334). The ``on_create`` sets ``password`` to None for old mysql_native_authentication but not for authentiation methods which uses the ``plugin*`` arguments. This PR changes this so ``on_create`` also exchange ``plugin``, ``plugin_hash_string``, ``plugin_auth_string`` to None in the list of arguments to change + +v3.2.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.2.0. + +Bugfixes +-------- + +- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + +v3.2.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.1.3. + +Major Changes +------------- + +- The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base 2.10``. While we take no active measures to prevent usage and there are no plans to introduce incompatible code to the modules, we will stop testing against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be End of Life and if you are still using them, you should consider upgrading to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/343). + +Minor Changes +------------- + +- mysql_user and mysql_role: Add the argument ``subtract_privs`` (boolean, default false, mutually exclusive with ``append_privs``). If set, the privileges given in ``priv`` are revoked and existing privileges are kept (https://github.com/ansible-collections/community.mysql/pull/333). + +Bugfixes +-------- + +- mysql_user - fix missing dynamic privileges after revoke and grant privileges to user (https://github.com/ansible-collections/community.mysql/issues/120). +- mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). + +v3.1.3 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.1.2. + +Bugfixes +-------- + +- mysql_replication - fails when using the `primary_use_gtid` option with `slave_pos` or `replica_pos` (https://github.com/ansible-collections/community.mysql/issues/335). +- mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). +- mysql_user - fix the possibility for a race condition that breaks certain (circular) replication configurations when ``DROP USER`` is executed on multiple nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287). + +v3.1.2 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.1.1. + +Bugfixes +-------- + +- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). + +v3.1.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.1.0. + +Bugfixes +-------- + +- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). + +v3.1.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 3.0.0. + +Minor Changes +------------- + +- Added explicit description of the supported versions of databases and connectors. Changes to the collection are **NOT** tested against database versions older than `mysql 5.7.31` and `mariadb 10.2.37` or connector versions older than `pymysql 0.7.10` and `mysqlclient 2.0.1`. (https://github.com/ansible-collections/community.mysql/discussions/141) +- mysql_user - added the ``force_context`` boolean option to set the default database context for the queries to be the ``mysql`` database. This way replication/binlog filters can catch the statements (https://github.com/ansible-collections/community.mysql/issues/265). + +Bugfixes +-------- + +- Collection core functions - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). + +v3.0.0 +====== + +Release Summary +--------------- + +This is the major release of the ``community.mysql`` collection. +This changelog contains all breaking changes to the modules in this collection +that have been added after the release of ``community.mysql`` 2.3.2. + +Breaking Changes / Porting Guide +-------------------------------- + +- mysql_replication - remove ``Is_Slave`` and ``Is_Master`` return values (were replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_replication - remove the mode options values containing ``master``/``slave`` and the master_use_gtid option ``slave_pos`` (were replaced with corresponding ``primary``/``replica`` values) (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_user - remove support for the `REQUIRESSL` special privilege as it has ben superseded by the `tls_requires` option (https://github.com/ansible-collections/community.mysql/discussions/121). +- mysql_user - validate privileges using database engine directly (https://github.com/ansible-collections/community.mysql/issues/234 https://github.com/ansible-collections/community.mysql/pull/243). Do not validate privileges in this module anymore. diff --git a/ansible_collections/community/mysql/CONTRIBUTING.md b/ansible_collections/community/mysql/CONTRIBUTING.md new file mode 100644 index 000000000..70cd5557e --- /dev/null +++ b/ansible_collections/community/mysql/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing + +Refer to the [Ansible Contributing guidelines](https://docs.ansible.com/ansible/devel/community/index.html) to learn how to contribute to this collection. + +Refer to the [review checklist](https://docs.ansible.com/ansible/devel/community/collection_contributors/collection_reviewing.html) when triaging issues or reviewing PRs. diff --git a/ansible_collections/community/mysql/CONTRIBUTORS b/ansible_collections/community/mysql/CONTRIBUTORS new file mode 100644 index 000000000..3acc8f33d --- /dev/null +++ b/ansible_collections/community/mysql/CONTRIBUTORS @@ -0,0 +1,284 @@ +116davinder +20 +28 +29 +4 +4n70w4 +abadger +abondis +acozine +adamchainz +adq +Akasurde +Alexander198961 +alustenberg +aminvakil +amitk79 +amree +Andersson007 +andrewhowdencom +ansibot +anthonyxpalermo +antonioribeiro +apollo13 +aquach +arcmop +asad-at-srt +AshDevilRed +aurimasl +axelll +axisK +azielke +baldpale +banyek +BarbzYHOOL +Berbe +bizmate +bjne +bmalynovytch +bmildren +bmillemathias +boreal321 +brutus +burner1024 +calfonso +candeira +caphrim007 +cdalbergue +checkphi +chrismeyersfsu +ChristopherGAndrews +cmodijk +codeaken +codebymikey +coreylane +CormacBracken +cosmix +cptMikky +crashes +dagwieers +damianmoore +Davidffry +denisemauldin +diclophis +d-lee +d-rupp +dmp1ce +dnelson +dramaley +drybjed +drzraf +DSpeichert +dungdm93 +dwagelaar +dylanjbarth +einarc +E-M +eowin +Ernest0x +esamattis +Everspace +F21 +faitno +felixfontein +flatrocks +fourjay +fraff +g00fy- +geerlingguy +georgeOsdDev +ghjm +ghost +giacmir +giorgio-v +gkoller +gottwald +gstorme +gundalow +hansbaer +hchargois +hluaces +hwali +hyperfocus1338 +igormukhingmailcom +imjoseangel +infigoKriti +int32bit +ipergenitsa +iredmail +ivandigiusto +jadbaz +jaikdean +jamescassell +janosmiko +jarnold-timeout +JaSafieddine +jb-2197 +jborean93 +jctanner +jean-christophe-manciot +Jean-Daniel +jgornick +jhagg +jhoekx +jirib +jkleckner +jkordish +jlaska +Jmainguy +jochu +JoelFeiner +johnavp1989 +jonatasbaldin +Jorge-Rodriguez +jpjaatin +jpmens +JSafieddine +jsmartin +juergenhoetzel +jw34 +kalaisubbiah +kenichi-ogawa-1988 +kkeane +klingac +koleo +kotso +kuntalFreshBooks +kurtdavis +larsks +laurent-indermuehle +ldesgrange +leeadh +LeonB +leucos +loomsen +lorin +lowwalker +lperezs +makmanalp +manuelmorena +MarcinOrlowski +markdorison +markotitel +marktheunissen +markuman +mattclay +matt-horwood-mayden +mavimo +maxamillion +maxbube +mcgoldrickm +meanstrong +meersjo +megamisan +michaeldg +michalmedvecky +MikeiLL +milky-milk +milosz +mistaka0s +mklassen +mkrizek +mmoya +mohag +mohsenSy +mpdehaan +MRwangyd +mverwijs +mvgrimes +mysqlbox +netmonk +nhojpatrick +nicolas-g +NielsH +nitinkansal1984 +nitzmahone +Ompragash +on +order +organman91 +p53 +pakal +paulbadcock +pennycoders +petoju +petracvv +pgrenaud +philfry +pileofrogs +pkaramol +platypus-geek +plumbeo +pratikgadiya12 +pshanbhag +r0bj +rajsshah86 +reduzent +relrod +resmo +ricco24 +richlv +riupie +rndmh3ro +robertdebock +robpblake +rokka-n +Roxyrob +roysmith +rsicart +rthouvenin +ruudk +samccann +samdoran +sayap +scottbrown +seanorama +sedrubal +sergey-trukhin +Shaps +shrikeh +sivel +skalfyfan +skoriy88 +sperantus +spoyd +steverweber +steveteahan +stijnopheide +stintel +stoned +strixBE +SWADESNA +tapologo +tarunm97 +tejatsk14 +tersmitten +the +the02 +thomasliddledba +time-palominodb +timorunge +Tomasthanes +tomdymond +Tronde +tuhoanganh +tvlooy +tyll +UncertaintyP +unnecessary-username +vamshi8 +vanne +vdboor +vmahadev +v-zhuravlev +webmat +wedi +whysthatso +willthames +windowsansiblernew +wrosario +xiata +Xyon +yangchao0512 +ziegenberg +Zverik diff --git a/ansible_collections/community/mysql/COPYING b/ansible_collections/community/mysql/COPYING new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/ansible_collections/community/mysql/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/ansible_collections/community/mysql/FILES.json b/ansible_collections/community/mysql/FILES.json new file mode 100644 index 000000000..e8a410307 --- /dev/null +++ b/ansible_collections/community/mysql/FILES.json @@ -0,0 +1,1769 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/ansible-test-plugins.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "853a324a5d61d6fcfcbacfdf8103802f3ec763c85d546b29b9d8351170069a83", + "format": 1 + }, + { + "name": ".github/workflows/ansible-test-roles.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a7935fe56bc37e6fd38a78d7db568c91ca3d1b7ce4cbdcb19a98ddaa56a48109", + "format": 1 + }, + { + "name": ".github/workflows/build-docker-image.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b18cd2b9626e53eeb0fa798cc4904f87fc604dcfa1289a940a5934be53dd4e7", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py310-mysqlclient211.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5899fa07d4981a81f4e6a4463ac097c96a415337ba78947ccd9133d0db38a4a", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py310-pymysql102.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "631e8098012e48a039ca551058a0add22f5008a5936a6cf7bf60878ae53d5555", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py38-mysqlclient201.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e364a04123f94d306119db070a0534a741459a10f47dff77f454c9022b3df58", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py38-pymysql093.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2190b0a8696f2345d871f9e3c0c45df56f30286349fa3f233c373519191601e1", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py39-mysqlclient203.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5fff2f277f492e845953ba61cf83ea4239778544716d29bddf21f9630d8243f5", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mariadb-py39-pymysql093.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1f117430b761ef86026dad5b1294f7e464de0a6c684b72f9e293f8b4b646ce99", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-my57-py38-mysqlclient201.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "236b0cb5a1b9db70f90e3a9db21624ca9c3a149696b922a7c9e67d681be74309", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-my57-py38-pymysql0711.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a71c2efc80739e7b1c77143eb08c5609e0e71ccdeb0ed4944ac82c3526ae1a57", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-my57-py38-pymysql093.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4d88583deff941ad46fec6ba1863554457549856b83e64f770e566bc9fdc61b2", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py310-mysqlclient211.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2025cc286a288274683f4648bf6935c276ac8823379f900baa965322ef36b2cc", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py310-pymysql102.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "649b0babbc8410ef6dcd45ccb65359726b664461540835ecfb6e0dc4add2f16d", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py38-mysqlclient201.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7fe2c47dd173c86f59dd2794d217a43a73141093b2978e6a0143294c291d692d", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py38-pymysql093.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3a51c095ff0464ad1fb7ab59ab592f783a222e69a46665dde203b4739895b0ba", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py39-mysqlclient203.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "436184d644a7842c7e85259506ac48e182afb98607b45438e26aff5fef4c109e", + "format": 1 + }, + { + "name": ".github/workflows/docker-image-mysql-py39-pymysql093.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "761051bdb7f90ec91931bc580e4d10f3da3c1eb60be7938884aad11a3d5f5e2e", + "format": 1 + }, + { + "name": ".github/patchback.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f28653c2f8d2965a55f76092049c4205a9c7f828e4edbd1cd089f7dd2685f93a", + "format": 1 + }, + { + "name": "changelogs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments/.keep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3508fbc7594b06c88b696e61e87b126ed3ccd549c50f9d63a34ed27b41d9122", + "format": 1 + }, + { + "name": "changelogs/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05fcc8854bde7615af15a7063d5f33389c445158b90ec61439e53bb2efaa56a9", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "df18179bb2f5447a56ac92261a911649b96821c0b2c08eea62d5cc6b0195203f", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/mysql.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "68615ead33b2ffd38c639a47980ae62ff6b7a3d9063ab322580072c6c7d5c452", + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/implementations", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mariadb", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mariadb/replication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a8065198e4191b4c391e1dc04999e331323e5c96ba17fce639d4c0c31785872", + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mariadb/role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b2f6a9fd17aeeb8905a59f7c164b2d357fcd19e6e9356953cd506eff0e28fad8", + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mariadb/user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "946bda63f668235fdc900f12cd3c4323054c7a45ef8aca91cd7379ace294bd7d", + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mysql", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mysql/replication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6c55a739ded9d9a2a4c15b27c6a21eb96aaabbf3f4a7497e338322d0d00b1c4b", + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mysql/role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "42841b3ffdd2bcb8f8c5416bacc6254a0d0140cdb1a0e4f2461330d9385173e5", + "format": 1 + }, + { + "name": "plugins/module_utils/implementations/mysql/user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f2f802810550b7d69e13d63f9002e524ab598e633c861caf424e7a5d8c293891", + "format": 1 + }, + { + "name": "plugins/module_utils/_version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1bdef9acb4f7b98135dee7366e5b26df32c52664175039b877a604b2865dadb5", + "format": 1 + }, + { + "name": "plugins/module_utils/database.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "647370826903ee88327f9faa84c007a44969c9d5093fcb979c7fbcacd1fea971", + "format": 1 + }, + { + "name": "plugins/module_utils/mysql.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5a8f8404b771e315d091a872e4369d2164c4b54e470ef496bd159caab1001e0c", + "format": 1 + }, + { + "name": "plugins/module_utils/user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8c93c05fecea1778fd0ef1d46bbc7d01f19742fbd4da8162d715fe6a0ed8a2e", + "format": 1 + }, + { + "name": "plugins/module_utils/version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d8cdb199b50593d3890fb415878a67ae267cbf00afdbe33d22e2d3d3e16d59e", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/mysql_db.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a0800f77595c95ea3fb253e9465606946aad289f5599da037f09e7b06c2b4ce", + "format": 1 + }, + { + "name": "plugins/modules/mysql_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c66d795038d4ee8fef281f942a936b939a1fbd3a2ba23e4d54ed7b3b47d7ddda", + "format": 1 + }, + { + "name": "plugins/modules/mysql_query.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b379afe3a4f5ea3d77c5738ad9f4426e33870a7f6d4b679200bfb72272460688", + "format": 1 + }, + { + "name": "plugins/modules/mysql_replication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "922d024797a958d73462c5bab30c4877db8a509d182402b9f336afad6f8d9a6c", + "format": 1 + }, + { + "name": "plugins/modules/mysql_role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c10005349310d333fb7911bfe357ce6d9d6d407d4e61f7b256c98f3a0cc93878", + "format": 1 + }, + { + "name": "plugins/modules/mysql_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ec672963bde8c6d512225bee75a06896a0d0719234d59068e7f4ec754f82cb2", + "format": 1 + }, + { + "name": "plugins/modules/mysql_variables.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a3301591b157740d3c77b8562cd278f8e626f3324741a98eaf14a740ea961135", + "format": 1 + }, + { + "name": "plugins/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "533dcaafc018b89297cf72098c36ea97dd2352a3f734fb8cc7568af8818fd7f1", + "format": 1 + }, + { + "name": "test-containers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py310-mysqlclient211", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py310-mysqlclient211/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8645448101f174b2fcfba4ea77000056341d58a3d28687790ecbe65c6055957c", + "format": 1 + }, + { + "name": "test-containers/mariadb-py310-pymysql102", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py310-pymysql102/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ae3b60a645296683fc8693badf6b62c6dc16e6e237b8e58f4035b84dca2954b", + "format": 1 + }, + { + "name": "test-containers/mariadb-py38-mysqlclient201", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py38-mysqlclient201/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d3cdf45fc5ccee87dd3315efd680595cf363dd98c42dbeca839faf3d9168153", + "format": 1 + }, + { + "name": "test-containers/mariadb-py38-pymysql093", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py38-pymysql093/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0e7b3fd704bba04941196211ac54536a48522e7abd53f839626f312f1a8045e3", + "format": 1 + }, + { + "name": "test-containers/mariadb-py39-mysqlclient203", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py39-mysqlclient203/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "add0b33c33c5611adb1f9a7a8222e391871adf751b38ee81e03990151d578873", + "format": 1 + }, + { + "name": "test-containers/mariadb-py39-pymysql093", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mariadb-py39-pymysql093/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4e7e4298e98fbf600e5a6e9b755f17291e5b1e042e0b7ca98de582eed188a7a2", + "format": 1 + }, + { + "name": "test-containers/my57-py38-mysqlclient201", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/my57-py38-mysqlclient201/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "826b4428976c39bdb7622ea57ab31377b64d873bf0cccb8d149d2b5a751a0ad1", + "format": 1 + }, + { + "name": "test-containers/my57-py38-pymysql0711", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/my57-py38-pymysql0711/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cda2562832062e172079171395381208919eb9b626e0fd42e084e9010ed84cab", + "format": 1 + }, + { + "name": "test-containers/my57-py38-pymysql093", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/my57-py38-pymysql093/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6bd79901c8b298683c0e4b7e799e8439804f5eeee65df0b4a2ae36b60b28c7ad", + "format": 1 + }, + { + "name": "test-containers/mysql-py310-mysqlclient211", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py310-mysqlclient211/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56423fc525a896bacb05a5e68cc0953e5ea4877a0678d6df43997741f6691c80", + "format": 1 + }, + { + "name": "test-containers/mysql-py310-pymysql102", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py310-pymysql102/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "503040c1e20a11b1102bb6a2c1e070391e86f83fe3b8e3963d65cfe4b6662ac0", + "format": 1 + }, + { + "name": "test-containers/mysql-py38-mysqlclient201", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py38-mysqlclient201/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22ae95aab1b367d6c85756e276dad4f1c075a987a3e6afd99496c73c6a2dd4e3", + "format": 1 + }, + { + "name": "test-containers/mysql-py38-pymysql093", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py38-pymysql093/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7b1bc490fbd53646fffaa3e99de0124b1c3d32c8064823d25d6270489beffb4e", + "format": 1 + }, + { + "name": "test-containers/mysql-py39-mysqlclient203", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py39-mysqlclient203/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb083b406eb6c559f0b499ca1bf11aa8e7758a8cc473b02543403c45ce9363fb", + "format": 1 + }, + { + "name": "test-containers/mysql-py39-pymysql093", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "test-containers/mysql-py39-pymysql093/Dockerfile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c1cabe0e67897a5ab4043e4c1d7a5a7e11aab0a3302a7a82756a93eafd0aa682", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb688e7705861f99d3908bc86da8805f9a698888fed0e0ccf7f1eb87f1024b18", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4dd18f7c3640db3c083acd4bccd974f5ee92ab73485be0b71fd3431feefcd0e3", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "44e2227e478af9f735498ec9805b1a1076885e4159dcaa21be440115948f0221", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8e284d5d7691066efd741bc8eb3ce6c918fd15adc56883a40013e771aa0331e5", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f449300e1412d0a776f4f0783f059acecbc8b3baba1fbeed990b239b91e4702e", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9e3a7741050f38e5ae170a22711b2cca3f5afd611c4e5feadfa1dc2310dd7648", + "format": 1 + }, + { + "name": "tests/integration/old_mariadb_replication/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8598fbccb4b6eaa7150d066f699e7bd937338c9de6cabbe1d630c7b9cf057ca4", + "format": 1 + }, + { + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller/tasks/fake_root.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91a13e0fd9dec01df150f9c842cc9050a95ce9bd5546c28d3e0fd612009c36b9", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "31cdc6ae00009f7aa176450c38ec621e9d6e64e71f2809f574c3066ca7b7cebf", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller/tasks/setvars.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f539f0d698d9bac7a4f87b5c8b9b7f49190c503c5dff29ef05632ff7745aeb04", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_controller/tasks/verify.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73cc400b068f45bc2449c791deb0bf602d3a1b5b93254326733b3ece4300c06b", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2d8c4cefde8b51b04fe7017d112b08c7f76124e75c277151ee0b7cf37794bcf6", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e273324ab90d72180a971d99b9ab69f08689c8be2e6adb991154fc294cf1056e", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2441ac1753320d2cd3bea299c160540e6ae31739ed235923ca478284d1fcfe09", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a59a3b5187a2df0991b1f689ab40d4ff937247260d6b6225440cb10c1cf9b859", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a69458421ccbf93a9eadb91e9fa7814b1c7550d6c6cca762454eb9ad46396c31", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "16f5ed51ee0f7237e65aa344d8899d3873facb989e615f25298fb0a3cb306c80", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6336cdff602b5c1080c3698510e69e674ffaffaf4c7dacec2815b122512dd0c8", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c0a173f8e1e5e82f31d5d24ad08bfe554aa7f437ca63b1272b10fa46eefaa47", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74a4e4ef95ac02e741141d6887639026da01a48072f36ba5132b451a5652f986", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fbf4f41edc443fa83f4b8c6160863ea9c17a769ec6b773852856942474c59e76", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "de7a207284516ec345853eedb135d000a798ff5d3344eca28d034cfcbdb3972d", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4eab01f0825b547657d0a670a92ee405a1de223ad69ae927c692d4e4b214ac2c", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b022e730780c4ff9071df0bebeaaf6789265e2ef6f9cfbabd1b5a23228b78e67", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ee2d1d5962a30355b1448f2ef32b7b4c618613de99f681925b6d7ff89a97690b", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "06ecaaa970b9fe6a6edb4cf1edd370c78b7402c67a96d3c6fd9130498c692863", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b5b531f2e9c4bf1a4c864e6827bd1283b680bad62786ebfb02f45af3cae643e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/tasks/connector_info.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d5ea7c5782d8513ee4106f02e59bb5187196f87622033c369c9f81dc7da26da8", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0179b33514e12f4b9d9a536bbbe12ae4959fd72a283a96221b4c78b030896f8", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ecfe0e433cd0468c3fab8f2da767b04effe068fa8b44a16c686801f3bd64feee", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_info/templates/my.cnf.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "28ce9e82a1512ce76200e2953ccfaf069f01a7ec915afbeec8c10a677c4f68d8", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9de7db3715b04f2cfd7546b1a347f6592dfcebf074636932e4a2b14ef61b08d2", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9b6b5dd01796cfa168cd43390d714aa4c177a50c601d7567bb87e0d745a0dfb", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8cc3fbdc8d4ee470f8dd96e17b2cee4334712e935352b4cae4c477a2ba53584c", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ebdbee2becdf8ca6e942e2ba3ab382a28353c204072b16fd6cee9446a8af6d3", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "55573c91713267dfd6b1b1328b222999c7c832a749bb2d0dd528794084cf5bf1", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3affb032256c7c19ecbdc80bdb805254a1028c3df50a58871b63c1511f851c16", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9b6b5dd01796cfa168cd43390d714aa4c177a50c601d7567bb87e0d745a0dfb", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/issue-265.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b1713ccf134da14fc73347e1e1c39b78273390c551275a6865725d269a7ae4b", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26c086e2a185db16fe85121478a63882461a7d0956ce0e5f6068850a782673e9", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9fb34e83a469331aa3a5e8d95e333203a67e2598512fc0d8ea504a050928a087", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f8bfabce4c19f9a6acc128ee84e8da916405bbed84f31f19e9d37986f8e596e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7855e5b87c17e4ce34c5718cee9c0be79b55abcf8ca342ae50bae03fde7e22a8", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b8289d0c2e0040301bfe27c4a64804cbc43fb62d522d42429e9e152929e4360", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "541ee3bf580bf03692e092ec2c7da25fe1efcb6d4bda9c72b09028a1c8b2af69", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4bfad1859eea36ca1d80390daea9b4465394ef4f2fe5331ccc6ae405eefc4326", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9b6b5dd01796cfa168cd43390d714aa4c177a50c601d7567bb87e0d745a0dfb", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "98aac442e4be66cf3a9600995379ff46a7b9957358c2402ff3bd9a84fc79e7e0", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd6434b60351d191008c9d54c490731e00b1afc42fc77bde20c153c703f2ad36", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "214f1ec054d98bc194e92d8480a2bf5c28f5b246443a0243116209f085188ddd", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e6c8fe9f93d608389522a25712b47d901890dcdb1d5d9eab071fd3a9e774685e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/files/create-function.sql", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f9ba9df87bc460be46fb77cabaceef9f93a73474d30e07d3d90648909d3c63f1", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/files/create-procedure.sql", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8386a2c5a55abf33d19484765d8354947773a81141e92434f8e736f766e6932d", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b5b531f2e9c4bf1a4c864e6827bd1283b680bad62786ebfb02f45af3cae643e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ebee7061dac1971208abaad666640669f56212afa8204478db797517f25ffa96", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "18506951cf15577c13daa90455f5a1e595181375efcc7c6adebee3ec8445089e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3c9f99ff4fdb6a488c0c37501e5d31b19e8f9fcff3807e6efb32bf8bd3bec26b", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5ced7539646e2602210ebd1d2de5ce0583ba4459a53920cd03ffc28642c0d05", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2a2d80d25463191eba758bd15ef43acdd4a2d6dc83bdff96d4446a58f882e038", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/issue-121.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9d360f608ee3888bca72833432e370e8f21141ed12ddbfebe5ba852610b8bba", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/issue-265.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d70eda8076edc8c3d8740c52fa9080fa9a7cf3755053811d4f53bf8cdd4c65c4", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b7610f209a6032afe69b8fbd791e1d79b16b5516ca5072436988a623e147d59a", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "59a6ead26f1d9c1a9b0ceaef94c41a0700bdb708f0538570395862e95bf8695c", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c97c8ce4a345bfd65d4c83cd2b7bd5024198161900d849fd7ada03bd8a94331c", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b471f216d58c90862f192e564df73859521fb7e4395ba38910fc1e53992d72e7", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6f4037db085710b0b501d5b3ffa994beae145450ab619105b2a8f585bcfc119", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ef24ba6ba9e408b904679d76bc71f56191387a73be8b72c39c7dce24e98aa7f2", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "99ec45a079172cec72605e11efb76ef59d7ccd6c7c2f0f8642301f0cc9180265", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d42c02597e70d6884ee51b1cf8dccef8860f16723d50349a7fdb295a7b9f74f7", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_privs.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "07cb8945c08b446911b0d252b18e7a1dc56c32715500deacb4b6da7c21e99f31", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1117aa83b2587d2ccaaf855f2d14d344a89249e73b33a24e3c346a2db4ffb4c1", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "971ed594d70daf937415782a71a2bbdeb053ecaed1363f52700aa65d65fc5dd4", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4be3059ceb6e3d358a40895482a1bd7c6d6f8643a8fe1f4a5022f77b85f45ad4", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c02d421e4d76e067e0d5704ad9be429aaf7e191adefee9ccba825d78e7d68644", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_update_password.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8adb77060c66a0c61d84304b71d3c2d462dfd673421b3ad05e433f9f64057635", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "00290cd9bf4b2dcf5b379d543d646d46916cdcfc1fbd6db772cc5fcd1d664218", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_user_password.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29f7c1388be05747adaffc567ecfe56379db8fed28e5f64e25ca09fad17a614e", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "829badccca0167fd8bfbc2e7be4662fc5c616588e61230fc68e7156b0c937b5c", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5d79850ff3396927ac1dba054396ee6b4e56a6448bc1abc0c86c665daeafde5d", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9b6b5dd01796cfa168cd43390d714aa4c177a50c601d7567bb87e0d745a0dfb", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91e923f38db01593064d9e115c94ca4c5fa7dd086c71923ba24f50c7d04b3c05", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/assert_var.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a5cc81982fa59ebaa9600f0946063e168eac80db7a0c698b1c221eddf50b332", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f84480cccf78d7452e2d78596d91b03dd5078efe1339b5ec79228cdffcb8aba", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/issue-28.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da908977cd40193665ab734933b9bdcf425158f5325afb0e2af61232c99888e9", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "409cf183d0203f6013be9abacbfc9576f717d6a9fce7a42210207f6dc6170f24", + "format": 1 + }, + { + "name": "tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "771383c701a5e6bbcf3b88b5fbad2b42363791ef693f20d80ad355158638e189", + "format": 1 + }, + { + "name": "tests/integration/test_connection.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "68d40ddfb362fec26cdbace8688fd0b8862c002498ffc6702104a90e43f90488", + "format": 1 + }, + { + "name": "tests/sanity", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26cf6bdacb30652f6105a97ed3695fbfcdce39966654c8f1179427f5ccd10267", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26cf6bdacb30652f6105a97ed3695fbfcdce39966654c8f1179427f5ccd10267", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26cf6bdacb30652f6105a97ed3695fbfcdce39966654c8f1179427f5ccd10267", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0db40238b384c184e5e3fc2261d8457fb174721ca6503da105ba173ae4ae203", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0db40238b384c184e5e3fc2261d8457fb174721ca6503da105ba173ae4ae203", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mariadb_replication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1241d3f1e777e37cc0bd653adc11fbb042a3898dc7b02e40cf3bbb31463f2db8", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mariadb_user_implementation.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "09f6933bab49f9a3c6996c7bc035a4fafc13298b02412ed137f86da306c7039b", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mysql.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "20d9ead509f38c46b09e931439e186ce1b678f9ca4ca9cc3242c725c20d05f57", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mysql_replication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b78cab4dbb60a84bea25bbc218e2d42445d8b52a6d078be257174e2ec5970463", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mysql_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e705d79f6e69be73ad67c40998df270780757ed8d42e3f36d66562dfceedc4a", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_mysql_user_implementation.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c68e6b43d46a5aad7d7054f07c889779ed7f68120f9b30d1b088405f6af03902", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_mysql_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d21791537625289a86302891669eae6260c6f06f2417e28d0345ffad0e988ffa", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_mysql_role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5c5b9bddce123b0b89559c6055062088282c987ee1553e651ac060fba85ecfc7", + "format": 1 + }, + { + "name": "tests/unit/plugins/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ba7638619ea90f4ac4f0f0a7639db544f4c4ca8c224bb1ccb4f7fd9cee953d66", + "format": 1 + }, + { + "name": ".gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ccca45309c57b7a20fe8b4731780361ff876c51517f6bac585145986146635bd", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b2aa4c6fb55f7d38aeb3774d4bff2108a8a4bfe71931dc540fd2ad55585baa8d", + "format": 1 + }, + { + "name": "CONTRIBUTING.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b0c50cf3715d59964a341dc651a6f626322209ef9fa8c0d03047d3a2b2e420a4", + "format": 1 + }, + { + "name": "CONTRIBUTORS", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2f07f95d703ea52b586842f9a918bf0c7319f8745c0b11099b7f4b6d334fb8cc", + "format": 1 + }, + { + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "MAINTAINERS", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "edb7885116c564025a692f93e694dcefa5af95ede853cd86e3aed5bf91bb5a7f", + "format": 1 + }, + { + "name": "MAINTAINING.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2435665a6562d5f3841fff1631970f95f0466c498e949d2b8579ccc2a0b810ad", + "format": 1 + }, + { + "name": "Makefile", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4e1a03f097e592d509ee6273a0a5d75a03b5299d7140386fd327fb8c8494a8f7", + "format": 1 + }, + { + "name": "PSF-license.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83b042fc7d6aca0f10d68e45efa56b9bc0a1496608e7e7728fe09d1a534a054a", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f9a28ac1c7026f63d93849398c90b42a1414cebb40e06efff39e64fd6e2e332", + "format": 1 + }, + { + "name": "TESTING.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d511d97260c97ffbd4ac1c83dda64b612a2924e19f2dd94b5fe473a3f0c4278d", + "format": 1 + }, + { + "name": "codecov.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74ef69719758a63944e959504d5396e442ed023c1c589ed987d9fc4676096d53", + "format": 1 + }, + { + "name": "run_all_tests.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d415065d0b22c6400ed42828f0da2083141467dbf40471145abe172b9b520756", + "format": 1 + }, + { + "name": "simplified_bsd.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f11e51ed1eec39ad21d458ba44d805807a301c17ee9fe39538ccc9e2b280936c", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/mysql/MAINTAINERS b/ansible_collections/community/mysql/MAINTAINERS new file mode 100644 index 000000000..2228e0044 --- /dev/null +++ b/ansible_collections/community/mysql/MAINTAINERS @@ -0,0 +1,6 @@ +betanummeric +bmalynovytch +Jorge-Rodriguez +rsicart +laurent-indermuehle +Andersson007 (andersson007_ in #ansible-community IRC/Matrix) diff --git a/ansible_collections/community/mysql/MAINTAINING.md b/ansible_collections/community/mysql/MAINTAINING.md new file mode 100644 index 000000000..9fad0d343 --- /dev/null +++ b/ansible_collections/community/mysql/MAINTAINING.md @@ -0,0 +1,3 @@ +# Maintaining this collection + +Refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). diff --git a/ansible_collections/community/mysql/MANIFEST.json b/ansible_collections/community/mysql/MANIFEST.json new file mode 100644 index 000000000..8df21709b --- /dev/null +++ b/ansible_collections/community/mysql/MANIFEST.json @@ -0,0 +1,32 @@ +{ + "collection_info": { + "namespace": "community", + "name": "mysql", + "version": "3.7.2", + "authors": [ + "Ansible community" + ], + "readme": "README.md", + "tags": [ + "database", + "mysql", + "mariadb" + ], + "description": "MySQL collection for Ansible", + "license": [], + "license_file": "COPYING", + "dependencies": {}, + "repository": "https://github.com/ansible-collections/community.mysql", + "documentation": "https://github.com/ansible-collections/community.mysql", + "homepage": "https://github.com/ansible-collections/community.mysql", + "issues": "https://github.com/ansible-collections/community.mysql/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d05eab7e7ecee1701730919ad3879343876d2b8c4eabd9abe159d0c771552a3", + "format": 1 + }, + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/mysql/Makefile b/ansible_collections/community/mysql/Makefile new file mode 100644 index 000000000..7ea0785dd --- /dev/null +++ b/ansible_collections/community/mysql/Makefile @@ -0,0 +1,112 @@ +SHELL := /bin/bash + +# To tell ansible-test and Make to not kill the containers on failure or +# end of tests. Disabled by default. +ifdef keep_containers_alive + _keep_containers_alive = --docker-terminate never +endif + +# This match what GitHub Action will do. Disabled by default. +ifdef continue_on_errors + _continue_on_errors = --retry-on-error --continue-on-error +endif + + +db_ver_tuple := $(subst ., , $(db_engine_version)) +db_engine_version_flat := $(word 1, $(db_ver_tuple))$(word 2, $(db_ver_tuple)) + +con_ver_tuple := $(subst ., , $(connector_version)) +connector_version_flat := $(word 1, $(con_ver_tuple))$(word 2, $(con_ver_tuple))$(word 3, $(con_ver_tuple)) + +py_ver_tuple := $(subst ., , $(python)) +python_version_flat := $(word 1, $(py_ver_tuple))$(word 2, $(py_ver_tuple)) + +ifeq ($(db_engine_version_flat), 57) + db_client := my57 +else + db_client := $(db_engine_name) +endif + + +.PHONY: test-integration +test-integration: + @echo -n $(db_engine_name) > tests/integration/db_engine_name + @echo -n $(db_engine_version) > tests/integration/db_engine_version + @echo -n $(connector_name) > tests/integration/connector_name + @echo -n $(connector_version) > tests/integration/connector_version + @echo -n $(python) > tests/integration/python + @echo -n $(ansible) > tests/integration/ansible + + # Create podman network for systems missing it. Error can be ignored + podman network create podman || true + podman run \ + --detach \ + --replace \ + --name primary \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3307:3306 \ + --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + mysqld + podman run \ + --detach \ + --replace \ + --name replica1 \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3308:3306 \ + --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + mysqld + podman run \ + --detach \ + --replace \ + --name replica2 \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3309:3306 \ + --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + mysqld + # Setup replication and restart containers + podman exec primary bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf' + podman exec replica1 bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf' + podman exec replica2 bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf' + # Don't restart a container unless it is healthy + while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 primary + while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 replica1 + while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 replica2 + while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + mkdir -p .venv/$(ansible) + python$(local_python_version) -m venv .venv/$(ansible) + + # Start venv (use `; \` to keep the same shell) + source .venv/$(ansible)/bin/activate; \ + python$(local_python_version) -m ensurepip; \ + python$(local_python_version) -m pip install --disable-pip-version-check \ + https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \ + set -x; \ + ansible-test integration $(target) -v --color --coverage --diff \ + --docker ghcr.io/ansible-collections/community.mysql/test-container\ + -$(db_client)-py$(python_version_flat)-$(connector_name)$(connector_version_flat):latest \ + --docker-network podman $(_continue_on_errors) $(_keep_containers_alive) --python $(python); \ + set +x + # End of venv + + rm tests/integration/db_engine_name + rm tests/integration/db_engine_version + rm tests/integration/connector_name + rm tests/integration/connector_version + rm tests/integration/python + rm tests/integration/ansible +ifndef keep_containers_alive + podman stop --time 0 --ignore primary replica1 replica2 + podman rm --ignore --volumes primary replica1 replica2 +endif diff --git a/ansible_collections/community/mysql/PSF-license.txt b/ansible_collections/community/mysql/PSF-license.txt new file mode 100644 index 000000000..35acd7fb5 --- /dev/null +++ b/ansible_collections/community/mysql/PSF-license.txt @@ -0,0 +1,48 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/ansible_collections/community/mysql/README.md b/ansible_collections/community/mysql/README.md new file mode 100644 index 000000000..5cb22712f --- /dev/null +++ b/ansible_collections/community/mysql/README.md @@ -0,0 +1,152 @@ +# MySQL collection for Ansible +[![Plugins CI](https://github.com/ansible-collections/community.mysql/workflows/Plugins%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Plugins+CI") [![Roles CI](https://github.com/ansible-collections/community.mysql/workflows/Roles%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Roles+CI") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) [![Discuss on Matrix at #mysql:ansible.com](https://img.shields.io/matrix/mysql:ansible.com.svg?server_fqdn=ansible-accounts.ems.host&label=Discuss%20on%20Matrix%20at%20%23mysql:ansible.com&logo=matrix)](https://matrix.to/#/#mysql:ansible.com) + +This collection is a part of the Ansible package. + +## Code of Conduct + +We follow the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project. + +If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint. + +## Contributing + +The content of this collection is made by [people](https://github.com/ansible-collections/community.mysql/blob/main/CONTRIBUTORS) just like you, a community of individuals collaborating on making the world better through developing automation software. + +We are actively accepting new contributors. + +Any kind of contribution is very welcome. + +You don't know how to start? Refer to our [contribution guide](https://github.com/ansible-collections/community.mysql/blob/main/CONTRIBUTING.md)! + +## Collection maintenance + +The current maintainers (contributors with `write` or higher access) are listed in the [MAINTAINERS](https://github.com/ansible-collections/community.mysql/blob/main/MAINTAINERS) file. If you have questions or need help, feel free to mention them in the proposals. + +To learn how to maintain / become a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible-collections/community.mysql/blob/main/MAINTAINING.md). + +It is necessary for maintainers of this collection to be subscribed to: + +* The collection itself (the `Watch` button -> `All Activity` in the upper right corner of the repository's homepage). +* The "Changes Impacting Collection Contributors and Maintainers" [issue](https://github.com/ansible-collections/overview/issues/45). + +They also should be subscribed to Ansible's [The Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn). + +## Communication + +We announce releases and important changes through Ansible's [The Bullhorn newsletter](https://eepurl.com/gZmiEP). Be sure you are subscribed. + +Join us on Matrix in the `#mysql:ansible.com` [room](https://matrix.to/#/#mysql:ansible.com), the `#users:ansible.com` [room](https://matrix.to/#/#users:ansible.com) (general use questions and support), `#ansible-community:ansible.com` [room](https://matrix.to/#/#community:ansible.com) (community and collection development questions), and other Matrix rooms or corresponding bridged Libera.Chat channels. See the [Ansible Communication Guide](https://docs.ansible.com/ansible/devel/community/communication.html) for details. + +We take part in the global quarterly [Ansible Contributor Summit](https://github.com/ansible/community/wiki/Contributor-Summit) virtually or in-person. Track [The Bullhorn newsletter](https://eepurl.com/gZmiEP) and join us. + +For more information about communication, refer to the [Ansible Communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). + +## Governance + +The process of decision making in this collection is based on discussing and finding consensus among participants. + +Every voice is important and every idea is valuable. If you have something on your mind, create an issue or dedicated discussion and let's discuss it! + +## Included content + +- **Modules**: + - [mysql_db](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_db_module.html) + - [mysql_info](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_info_module.html) + - [mysql_query](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_query_module.html) + - [mysql_replication](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_replication_module.html) + - [mysql_role](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_role_module.html) + - [mysql_user](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_user_module.html) + - [mysql_variables](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_variables_module.html) + + +## Releases Support Timeline + +It has been [decided](https://github.com/ansible-collections/community.mysql/discussions/537) to maintain each major release (1.x.y, 2.x.y, ...) for two years after the next major version is released. + +Here is the table for the support timeline: + +- 1.x.y: released 2020-08-17, EOL +- 2.x.y: released 2021-04-15, supported until 2023-12-01 +- 3.x.y: released 2021-12-01, current +- 4.x.y: To be released + + +## Tested with + +### ansible-core + +- 2.12 +- 2.13 +- 2.14 +- current development version + +### Databases + +For MariaDB, only Long Term releases are tested. + +- mysql 5.7.40 +- mysql 8.0.31 +- mariadb:10.3.34 (only collection version <= 3.5.1) +- mariadb:10.4.24 (only collection version >= 3.5.2) +- mariadb:10.5.18 (only collection version >= 3.5.2) +- mariadb:10.6.11 (only collection version >= 3.5.2) +- mariadb:10.11.?? (waiting for release) + + +### Database connectors + +- pymysql 0.7.11 (Only tested with MySQL 5.7) +- pymysql 0.9.3 +- pymysql 1.0.2 (only collection version >= 3.6.1) +- mysqlclient 2.0.1 +- mysqlclient 2.0.3 (only collection version >= 3.5.2) +- mysqlclient 2.1.1 (only collection version >= 3.5.2) + +## External requirements + +The MySQL modules rely on a MySQL connector. The list of supported drivers is below: + +- [PyMySQL](https://github.com/PyMySQL/PyMySQL) +- [mysqlclient](https://github.com/PyMySQL/mysqlclient) +- Support for other Python MySQL connectors may be added in a future release. + +## Using this collection + +### Installing the Collection from Ansible Galaxy + +Before using the MySQL collection, you need to install it with the Ansible Galaxy CLI: + +```bash +ansible-galaxy collection install community.mysql +``` + +You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml`, using the format: + +```yaml +--- +collections: + - name: community.mysql +``` + +Note that if you install the collection from Ansible Galaxy, it will not be upgraded automatically if you upgrade the Ansible package. To upgrade the collection to the latest available version, run the following command: + +```bash +ansible-galaxy collection install community.mysql --upgrade +``` + +You can also install a specific version of the collection, for example, if you need to downgrade when something is broken in the latest version (please report an issue in this repository). Use the following syntax: + +```bash +ansible-galaxy collection install community.mysql:==2.0.0 +``` + +See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. + +## Licensing + +<!-- Include the appropriate license information here and a pointer to the full licensing details. If the collection contains modules migrated from the ansible/ansible repo, you must use the same license that existed in the ansible/ansible repo. See the GNU license example below. --> + +GNU General Public License v3.0 or later. + +See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text. diff --git a/ansible_collections/community/mysql/TESTING.md b/ansible_collections/community/mysql/TESTING.md new file mode 100644 index 000000000..7bbafc31d --- /dev/null +++ b/ansible_collections/community/mysql/TESTING.md @@ -0,0 +1,166 @@ +# Tests + +This collection uses GitHub Actions to run ansible-test to validate its content. Three type of tests are used: Sanity, Integration and Units. + +The tests covers plugins and roles (no role available yet, but tests are ready) and can be found here: + +- Plugins: *.github/workflows/ansible-test-plugins.yml* +- Roles: *.github/workflows/ansible-test-roles.yml* (unused yet) + +Everytime you push on your fork or you create a pull request, both workflows runs. You can see the output on the "Actions" tab. + + +## Integration tests + +You can use GitHub to run ansible-test either on the community repo or your fork. But sometimes you want to quickly test a single version or a single target. To do that, you can use the Makefile present at the root of this repository. + +For now, the makefile only supports Podman. + + +### Requirements + +- python >= 3.8 and <= 3.10 +- make +- podman +- Minimum 15GB of free space on the device storing containers images and volumes. You can use this command to check: `podman system info --format='{{.Store.GraphRoot}}'|xargs findmnt --noheadings --nofsroot --output SOURCE --target|xargs df -h --output=size,used,avail,pcent,target` +- Minimum 2GB of RAM + + +### Custom ansible-test containers + +Our integrations tests use custom containers for ansible-test. Those images have their definition file stored in the directory [test-containers](test-containers/). We build and publish the images on ghcr.io under the ansible-collection namespace: E.G.: +`ghcr.io/ansible-collections/community.mysql/test-container-mariadb106-py310-mysqlclient211:latest`. + +Availables images are listed [here](https://github.com/orgs/ansible-collections/packages). + + +### Makefile options + +The Makefile accept the following options + +- `local_python_version` + - Mandatory: false + - Choices: + - "3.8" + - "3.9" + - "3.10" + - Description: If `Python -V` shows an unsupported version, use this option and choose one of the version available on your system. Use `ls /usr/bin/python3*|grep -v config` to list them. + +- `ansible` + - Mandatory: true + - Choices: + - "stable-2.12" + - "stable-2.13" + - "stable-2.14" + - "devel" + - Description: Version of ansible to install in a venv to run ansible-test + +- `db_engine_name` + - Mandatory: true + - Choices: + - "mysql" + - "mariadb" + - Description: The name of the database engine to use for the service containers that will host a primary database and two replicas. + +- `db_engine_version` + - Mandatory: true + - Choices: + - "5.7.40" <- mysql + - "8.0.31" <- mysql + - "10.4.24" <- mariadb + - "10.5.18" <- mariadb + - "10.6.11" <- mariadb + - Description: The tag of the container to use for the service containers that will host a primary database and two replicas. Do not use short version, like `mysql:8` (don't do that) because our tests expect a full version to filter tests precisely. For instance: `when: db_version is version ('8.0.22', '>')`. You can use any tag available on [hub.docker.com/_/mysql](https://hub.docker.com/_/mysql) and [hub.docker.com/_/mariadb](https://hub.docker.com/_/mariadb) but GitHub Action will only use the versions listed above. + +- `connector_name` + - Mandatory: true + - Choices: + - "pymysql + - "mysqlclient" + - Description: The python package of the connector to use. In addition to selecting the test container, this value is also used for tests filtering: `when: connector_name == 'pymysql'`. + +- `connector_version` + - Mandatory: true + - Choices: + - "0.7.11" <- pymysql (Only for MySQL 5.7) + - "0.9.3" <- pymysql + - "1.0.2" <- pymysql + - "2.0.1" <- mysqlclient + - "2.0.3" <- mysqlclient + - "2.1.1" <- mysqlclient + - Description: The version of the python package of the connector to use. This value is used to filter tests meant for other connectors. + +- `python` + - Mandatory: true + - Choices: + - "3.8" + - "3.9" + - "3.10" + - Description: The python version to use in the controller (ansible-test container). + +- `target` + - Mandatory: false + - Choices: + - "test_mysql_db" + - "test_mysql_info" + - "test_mysql_query" + - "test_mysql_replication" + - "test_mysql_role" + - "test_mysql_user" + - "test_mysql_variables" + - Description: If omitted, all test targets will run. But you can limit the tests to a single target to speed up your tests. + +- `keep_containers_alive` + - Mandatory: false + - Description: This option keeps all tree databases containers and the ansible-test container alive at the end of tests or in case of failure. This is useful to enter one of the containers with `podman exec -it <container-name> bash` for debugging. Rerunning the +tests will overwrite the 3 databases containers so no need to kill them in advance. But nothing will kill the ansible-test container. You must do that using `podman stop` and `podman rm`. Add any value to activate this option: `keep_containers_alive=1` + +- `continue_on_errors` + - Mandatory: false + - Description: Tells ansible-test to retry on errors and also continue on errors. This is the way the GitHub Action's workflow runs the tests. This can be used to catch all errors in a single run, but you'll need to scroll up to find them. Add any value to activate this option: `continue_on_errors=1` + + +#### Makefile usage examples: + +```sh +# Run all targets +make ansible="stable-2.12" db_engine_name="mysql" db_engine_version="5.7.40" python="3.8" connector_name="pymysql" connector_version="0.7.11" + +# A single target +make ansible="stable-2.14" db_engine_name="mysql" db_engine_version="5.7.40" python="3.8" connector_name="pymysql" connector_version="0.7.11" target="test_mysql_info" + +# Keep databases and ansible tests containers alives +# A single target and continue on errors +make ansible="stable-2.14" db_engine_name="mysql" db_engine_version="8.0.31" python="3.9" connector_name="mysqlclient" connector_version="2.0.3" target="test_mysql_query" keep_containers_alive=1 continue_on_errors=1 + +# If your system has an usupported version of Python: +make local_python_version="3.8" ansible="stable-2.14" db_engine_name="mariadb" db_engine_version="10.6.11" python="3.9" connector_name="pymysql" connector_version="0.9.3" +``` + + +### Run all tests + +GitHub Action offer a test matrix that run every combination of Python, MySQL, MariaDB and Connector against each other. To reproduce this, this repo provides a script called *run_all_tests.py*. + +Examples: + +```sh +python run_all_tests.py +``` + + +### Add a new Python, Connector or Database version + +You can look into `[.github/workflows/ansible-test-plugins.yml](https://github.com/ansible-collections/community.mysql/tree/main/.github/workflows)` to see how those containers are built using [build-docker-image.yml](https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/build-docker-image.yml) and all [docker-image-xxx.yml](https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/docker-image-mariadb103-py38-mysqlclient201.yml) files. + +1. Add a workflow in [.github/workflows/](.github/workflows) +1. Add a new folder in [test-containers](test-containers) containing a new Dockerfile. Your container must contains 3 things: + - Python + - A connector: The python package to connect to the database (pymysql, mysqlclient, ...) + - A mysql client to prepare databases before our tests starts. This client must provide both `mysql` and `mysqldump` commands. +1. Add your version in the matrix of *.github/workflows/ansible-test-plugins.yml*. You can use [run_all_tests.py](run_all_tests.py) to help you see what the matrix will be. Simply comment out the line `os.system(make_cmd)` before runing the script. You can also add `print(len(matrix))` to display how many tests there will be on GitHub Action. +1. Ask the lead maintainer to mark your new image(s) as `public` under [https://github.com/orgs/ansible-collections/packages](https://github.com/orgs/ansible-collections/packages) + +After pushing your commit to the remote, the container will be built and published on ghcr.io. Have a look in the "Action" tab to see if it worked. In case of error `failed to copy: io: read/write on closed pipe` re-run the workflow, this append unfortunately a lot. + +To see the docker image produced, go to the package page in the ansible-collection namespace [https://github.com/orgs/ansible-collections/packages](https://github.com/orgs/ansible-collections/packages). This page indicate a "Published x days ago" that is updated infrequently. To see the last time the container has been updated you must click on its title and look in the right hands side bellow the title "Last published". diff --git a/ansible_collections/community/mysql/changelogs/changelog.yaml b/ansible_collections/community/mysql/changelogs/changelog.yaml new file mode 100644 index 000000000..e3431f34f --- /dev/null +++ b/ansible_collections/community/mysql/changelogs/changelog.yaml @@ -0,0 +1,348 @@ +ancestor: 2.0.0 +releases: + 3.0.0: + changes: + breaking_changes: + - mysql_replication - remove ``Is_Slave`` and ``Is_Master`` return values (were + replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections/community.mysql/issues/145). + - mysql_replication - remove the mode options values containing ``master``/``slave`` + and the master_use_gtid option ``slave_pos`` (were replaced with corresponding + ``primary``/``replica`` values) (https://github.com/ansible-collections/community.mysql/issues/145). + - mysql_user - remove support for the `REQUIRESSL` special privilege as it has + ben superseded by the `tls_requires` option (https://github.com/ansible-collections/community.mysql/discussions/121). + - mysql_user - validate privileges using database engine directly (https://github.com/ansible-collections/community.mysql/issues/234 + https://github.com/ansible-collections/community.mysql/pull/243). Do not validate + privileges in this module anymore. + release_summary: 'This is the major release of the ``community.mysql`` collection. + + This changelog contains all breaking changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 2.3.2.' + fragments: + - 243-get-rid-of-privs-comparison.yml + - 244-remove-requiressl-privilege.yaml + - 3.0.0.yml + - 300-mysql_replication_remove_master_slave.yml + release_date: '2021-12-01' + 3.1.0: + changes: + bugfixes: + - Collection core functions - use vendored version of ``distutils.version`` + instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). + minor_changes: + - Added explicit description of the supported versions of databases and connectors. + Changes to the collection are **NOT** tested against database versions older + than `mysql 5.7.31` and `mariadb 10.2.37` or connector versions older than + `pymysql 0.7.10` and `mysqlclient 2.0.1`. (https://github.com/ansible-collections/community.mysql/discussions/141) + - mysql_user - added the ``force_context`` boolean option to set the default + database context for the queries to be the ``mysql`` database. This way replication/binlog + filters can catch the statements (https://github.com/ansible-collections/community.mysql/issues/265). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.0.0.' + fragments: + - 141-supported-database-and-connector-versions.yaml + - 266-default-database-for-mysql-user.yml + - 267-prepare_for_distutils_be_removed.yml + - 3.1.0.yml + release_date: '2022-01-18' + 3.1.1: + changes: + bugfixes: + - mysql_role - make the ``set_default_role_all`` parameter actually working + (https://github.com/ansible-collections/community.mysql/pull/282). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.1.0.' + fragments: + - 282-mysql_role_fix_set_default_role_all_argument.yml + - 3.1.1.yml + release_date: '2022-02-16' + 3.1.2: + changes: + bugfixes: + - Collection core functions - fixes related to the mysqlclient Python connector + (https://github.com/ansible-collections/community.mysql/issues/292). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.1.1.' + fragments: + - 0-mysqlclient.yml + - 3.1.2.yml + release_date: '2022-03-14' + 3.1.3: + changes: + bugfixes: + - mysql_replication - fails when using the `primary_use_gtid` option with `slave_pos` + or `replica_pos` (https://github.com/ansible-collections/community.mysql/issues/335). + - mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). + - 'mysql_user - fix the possibility for a race condition that breaks certain + (circular) replication configurations when ``DROP USER`` is executed on multiple + nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: + no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287).' + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.1.2.' + fragments: + - 0-mysql_replication_replica_pos.yml + - 3.1.3.yml + - 307-mysql_user_add_if_exists_to_drop.yml + - 329-mysql_role-remove-redudant-connection-closing.yml + release_date: '2022-04-26' + 3.2.0: + changes: + bugfixes: + - mysql_user - fix missing dynamic privileges after revoke and grant privileges + to user (https://github.com/ansible-collections/community.mysql/issues/120). + - mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). + major_changes: + - The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base + 2.10``. While we take no active measures to prevent usage and there are no + plans to introduce incompatible code to the modules, we will stop testing + against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be + End of Life and if you are still using them, you should consider upgrading + to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible + (https://github.com/ansible-collections/community.mysql/pull/343). + minor_changes: + - 'mysql_user and mysql_role: Add the argument ``subtract_privs`` (boolean, + default false, mutually exclusive with ``append_privs``). If set, the privileges + given in ``priv`` are revoked and existing privileges are kept (https://github.com/ansible-collections/community.mysql/pull/333).' + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.1.3.' + fragments: + - 001-mysql_user_fix_pars_users_with_roles_assigned.yml + - 3.2.0.yml + - 333-mysql_user-mysql_role-add-subtract_privileges-argument.yml + - 338-mysql_user_fix_missing_dynamic_privileges.yml + - drop_support_of_2.9-2.10.yml + release_date: '2022-05-13' + 3.2.1: + changes: + bugfixes: + - Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.2.0.' + fragments: + - 3.2.1.yml + - psf-license.yml + release_date: '2022-05-17' + 3.3.0: + changes: + bugfixes: + - mysql_query - fix false change reports when ``IF EXISTS/IF NOT EXISTS`` clause + is used (https://github.com/ansible-collections/community.mysql/issues/268). + - 'mysql_role - don''t add members to a role when creating the role and ``detach_members: + true`` is set (https://github.com/ansible-collections/community.mysql/pull/367).' + - 'mysql_role - in some cases (when "SHOW GRANTS" did not use backticks for + quotes), no unwanted members were detached from the role (and redundant "GRANT" + statements were executed for wanted members). This is fixed by querying the + existing role members from the mysql.role_edges (MySQL) or mysql.roles_mapping + (MariaDB) tables instead of parsing the "SHOW GRANTS" output (https://github.com/ansible-collections/community.mysql/pull/368). + + ' + - mysql_user - fix logic when ``update_password`` is set to ``on_create`` for + users using ``plugin*`` arguments (https://github.com/ansible-collections/community.mysql/issues/334). + The ``on_create`` sets ``password`` to None for old mysql_native_authentication + but not for authentiation methods which uses the ``plugin*`` arguments. This + PR changes this so ``on_create`` also exchange ``plugin``, ``plugin_hash_string``, + ``plugin_auth_string`` to None in the list of arguments to change + minor_changes: + - 'mysql_role - add the argument ``members_must_exist`` (boolean, default true). + The assertion that the users supplied in the ``members`` argument exist is + only executed when the new argument ``members_must_exist`` is ``true``, to + allow opt-out (https://github.com/ansible-collections/community.mysql/pull/369). + + ' + - 'mysql_user - Add the option ``on_new_username`` to argument ``update_password`` + to reuse the password (plugin and authentication_string) when creating a new + user if some user with the same name already exists. If the existing user + with the same name have varying passwords, the password from the arguments + is used like with ``update_password: always`` (https://github.com/ansible-collections/community.mysql/pull/365). + + ' + - 'mysql_user - Add the result field ``password_changed`` (boolean). It is true, + when the user got a new password. When the user was created with ``update_password: + on_new_username`` and an existing password was reused, ``password_changed`` + is false (https://github.com/ansible-collections/community.mysql/pull/365). + + ' + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.2.1.' + fragments: + - 3.3.0.yml + - 322-mysql_query_fix_false_change_report.yml + - 334-mysql_user_fix_logic_on_oncreate.yml + - 365-mysql_user-add-on_new_username-and-password_changed.yml + - 367-mysql_role-fix-deatch-members.yml + - 368-mysql_role-fix-member-detection.yml + - 369_mysql_role-add-members_must_exist.yml + release_date: '2022-06-02' + 3.4.0: + changes: + bugfixes: + - Include ``simplified_bsd.txt`` license file for various module utils. + - mysql_db - Using compression masks errors messages from mysql_dump. By default + the fix is inactive to ensure retro-compatibility with system without bash. + To activate the fix, use the module option ``pipefail=true`` (https://github.com/ansible-collections/community.mysql/issues/256). + - mysql_replication - when the ``primary_ssl`` argument is set to ``no``, the + module will turn off SSL (https://github.com/ansible-collections/community.mysql/issues/393). + major_changes: + - mysql_db - the ``pipefail`` argument's default value will be changed to ``true`` + in community.mysql 4.0.0. If your target machines do not use ``bash`` as a + default interpreter, set ``pipefail`` to ``false`` explicitly. However, we + strongly recommend setting up ``bash`` as a default and ``pipefail=true`` + as it will protect you from getting broken dumps you don't know about (https://github.com/ansible-collections/community.mysql/issues/407). + minor_changes: + - mysql_db - add the ``chdir`` argument to avoid failings when a dump file contains + relative paths (https://github.com/ansible-collections/community.mysql/issues/395). + - mysql_db - add the ``pipefail`` argument to avoid broken dumps when ``state`` + is ``dump`` and compression is used (https://github.com/ansible-collections/community.mysql/issues/256). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.3.0.' + fragments: + - 0-mysql_db_add_chdir_argument.yml + - 1-mysql_replication_can_disable_master_ssl.yml + - 2-mysql_db_announce.yml + - 3.4.0.yml + - fix-256-mysql_dump-errors.yml + - simplified-bsd-license.yml + release_date: '2022-08-02' + 3.5.0: + changes: + bugfixes: + - mysql_user - grant option was revoked accidentally when modifying users. This + fix revokes grant option only when privs are setup to do that (https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807). + minor_changes: + - 'mysql_replication - add a new option: ``primary_ssl_verify_server_cert`` + (https://github.com//pull/435).' + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 3.4.0.' + fragments: + - 3.5.0.yml + - 434-do-not-revoke-grant-option-always.yaml + - 435-mysql_replication_verify_server_cert.yml + release_date: '2022-09-05' + 3.5.1: + changes: + bugfixes: + - mysql_user, mysql_role - mysql/mariadb recent versions translate 'ALL PRIVILEGES' + to a list of specific privileges. That caused a change every time we modified + user privileges. This fix compares privs before and after user modification + to avoid this infinite change (https://github.com/ansible-collections/community.mysql/issues/77). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.5.1.yml + - 438-fix-privilege-changing-everytime.yml + release_date: '2022-09-09' + 3.6.0: + changes: + bugfixes: + - mysql_user - when revoke privs consists only of ``GRANT``, a 2nd revoke query + is executed with empty privs to revoke that ended in an SQL exception (https://github.com/ansible-collections/community.mysql/pull/503). + - mysql_variables - add uppercase character pattern to regex to allow GLOBAL + variables containing uppercase characters. This recognizes variable names + used in Galera, for example, ``wsrep_OSU_method``, which breaks the normal + pattern of all lowercase characters (https://github.com/ansible-collections/community.mysql/pull/501). + minor_changes: + - mysql_info - add ``connector_name`` and ``connector_version`` to returned + values (https://github.com/ansible-collections/community.mysql/pull/497). + - mysql_role - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). + - mysql_user - add plugin_auth_string as optional parameter to use a specific + pam service if pam/auth_pam plugin is used (https://github.com/ansible-collections/community.mysql/pull/445). + - mysql_user - add the ``session_vars`` argument to set session variables at + the beginning of module execution (https://github.com/ansible-collections/community.mysql/issues/478). + - mysql_user - display a more informative invalid privilege exception. Changes + the exception handling of the granting permission logic to show the query + executed , params and the exception message granting privileges fails` (https://github.com/ansible-collections/community.mysql/issues/465). + - mysql_user - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). + - setup_mysql - update MySQL tarball URL (https://github.com/ansible-collections/community.mysql/pull/491). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 0_mysql_user_session_vars.yml + - 3.6.0.yml + - 445_add_service_name_to_plugin_pam_auth_pam_usage.yml + - 465-display_more_informative_invalid_priv_exceptiion.yml + - 479_enable_auto_commit.yml + - 479_enable_auto_commit_part2.yml + - 491_fix_download_url.yaml + - 497_mysql_info_returns_connector_name_and_version.yml + - 503-fix-revoke-grant-only.yml + - mysql_variables_allow_uppercase_identifiers.yml + release_date: '2023-02-08' + 3.7.0: + changes: + minor_changes: + - mysql module utils - change deprecated connection parameters ``passwd`` and + ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/177). + - mysql_user - add ``MAX_STATEMENT_TIME`` support for mariadb to the ``resource_limits`` + argument (https://github.com/ansible-collections/community.mysql/issues/211). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 177-change_deprecated_connection_parameters.yml + - 3.7.0.yml + - 523-add-max_statement_time_resource-limit.yml + release_date: '2023-05-05' + 3.7.1: + changes: + bugfixes: + - mysql module utils - use the connection arguments ``db`` instead of ``database`` + and ``passwd`` instead of ``password`` when running with older mysql drivers + (MySQLdb < 2.1.0 or PyMySQL < 1.0.0) (https://github.com/ansible-collections/community.mysql/pull/551). + release_summary: 'This is a patch release of the community.mysql collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.7.1.yml + - 551-fix_connection_arguments_driver_compatability.yaml + release_date: '2023-05-22' + 3.7.2: + changes: + bugfixes: + - mysql module utils - use the connection arguments ``db`` instead of ``database`` + and ``passwd`` instead of ``password`` when running with MySQLdb < 2.0.0 (https://github.com/ansible-collections/community.mysql/pull/553). + release_summary: 'This is a patch release of the community.mysql collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.7.2.yml + - 553_fix_connection_arguemnts_for_old_mysqldb_driver.yaml + release_date: '2023-05-25' diff --git a/ansible_collections/community/mysql/changelogs/config.yaml b/ansible_collections/community/mysql/changelogs/config.yaml new file mode 100644 index 000000000..70ab03685 --- /dev/null +++ b/ansible_collections/community/mysql/changelogs/config.yaml @@ -0,0 +1,29 @@ +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +sections: +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features (previously deprecated) +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues +title: Community MySQL Collection +trivial_section_name: trivial diff --git a/ansible_collections/community/mysql/changelogs/fragments/.keep b/ansible_collections/community/mysql/changelogs/fragments/.keep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/mysql/changelogs/fragments/.keep diff --git a/ansible_collections/community/mysql/codecov.yml b/ansible_collections/community/mysql/codecov.yml new file mode 100644 index 000000000..e832c21f7 --- /dev/null +++ b/ansible_collections/community/mysql/codecov.yml @@ -0,0 +1,2 @@ +fixes: + - "/ansible_collections/community/mysql/::" diff --git a/ansible_collections/community/mysql/meta/runtime.yml b/ansible_collections/community/mysql/meta/runtime.yml new file mode 100644 index 000000000..2ee3c9fa9 --- /dev/null +++ b/ansible_collections/community/mysql/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: '>=2.9.10' diff --git a/ansible_collections/community/mysql/plugins/README.md b/ansible_collections/community/mysql/plugins/README.md new file mode 100644 index 000000000..5b4711b5f --- /dev/null +++ b/ansible_collections/community/mysql/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/latest/plugins/plugins.html). diff --git a/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py b/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py new file mode 100644 index 000000000..939126cba --- /dev/null +++ b/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Jonathan Mainguy <jon@soh.re> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard mysql documentation fragment + DOCUMENTATION = r''' +options: + login_user: + description: + - The username used to authenticate with. + type: str + login_password: + description: + - The password used to authenticate with. + type: str + login_host: + description: + - Host running the database. + - In some cases for local connections the I(login_unix_socket=/path/to/mysqld/socket), + that is usually C(/var/run/mysqld/mysqld.sock), needs to be used instead of I(login_host=localhost). + type: str + default: localhost + login_port: + description: + - Port of the MySQL server. Requires I(login_host) be defined as other than localhost if login_port is used. + type: int + default: 3306 + login_unix_socket: + description: + - The path to a Unix domain socket for local connections. + - Use this parameter to avoid the C(Please explicitly state intended protocol) error. + type: str + connect_timeout: + description: + - The connection timeout when connecting to the MySQL server. + type: int + default: 30 + config_file: + description: + - Specify a config file from which user and password are to be read. + - The default config file, C(~/.my.cnf), if it exists, will be read, even if I(config_file) is not specified. + - The default config file, C(~/.my.cnf), if it exists, must contain a C([client]) section as a MySQL connector requirement. + - To prevent the default config file from being read, set I(config_file) to be an empty string. + type: path + default: '~/.my.cnf' + ca_cert: + description: + - The path to a Certificate Authority (CA) certificate. This option, if used, must specify the same certificate + as used by the server. + type: path + aliases: [ ssl_ca ] + client_cert: + description: + - The path to a client public key certificate. + type: path + aliases: [ ssl_cert ] + client_key: + description: + - The path to the client private key. + type: path + aliases: [ ssl_key ] + check_hostname: + description: + - Whether to validate the server host name when an SSL connection is required. Corresponds to MySQL CLIs C(--ssl) switch. + - Setting this to C(false) disables hostname verification. Use with caution. + - Requires pymysql >= 0.7.11. + - This option has no effect on MySQLdb. + type: bool + version_added: '1.1.0' +requirements: + - mysqlclient (Python 3.5+) or + - PyMySQL (Python 2.7 and Python 3.x) or + - MySQLdb (Python 2.x) +notes: + - Requires the PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) package installed on the remote host. + The Python package may be installed with apt-get install python-pymysql (Ubuntu; see M(ansible.builtin.apt)) or + yum install python2-PyMySQL (RHEL/CentOS/Fedora; see M(ansible.builtin.yum)). You can also use dnf install python2-PyMySQL + for newer versions of Fedora; see M(ansible.builtin.dnf). + - Be sure you have mysqlclient, PyMySQL, or MySQLdb library installed on the target machine + for the Python interpreter Ansible discovers. For example if ansible discovers and uses Python 3, you need to install + the Python 3 version of PyMySQL or mysqlclient. If ansible discovers and uses Python 2, you need to install the Python 2 + version of either PyMySQL or MySQL-python. + - If you have trouble, it may help to force Ansible to use the Python interpreter you need by specifying + C(ansible_python_interpreter). For more information, see + U(https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html). + - Both C(login_password) and C(login_user) are required when you are + passing credentials. If none are present, the module will attempt to read + the credentials from C(~/.my.cnf), and finally fall back to using the MySQL + default login of 'root' with no password. + - If there are problems with local connections, using I(login_unix_socket=/path/to/mysqld/socket) + instead of I(login_host=localhost) might help. As an example, the default MariaDB installation of version 10.4 + and later uses the unix_socket authentication plugin by default that + without using I(login_unix_socket=/var/run/mysqld/mysqld.sock) (the default path) + causes the error ``Host '127.0.0.1' is not allowed to connect to this MariaDB server``. + - Alternatively, you can use the mysqlclient library instead of MySQL-python (MySQLdb) + which supports both Python 2.X and Python >=3.5. + See U(https://pypi.org/project/mysqlclient/) how to install it. + - "If credentials from the config file (for example, C(/root/.my.cnf)) are not needed to connect to a database server, but + the file exists and does not contain a C([client]) section, before any other valid directives, it will be read and this + will cause the connection to fail, to prevent this set it to an empty string, (for example C(config_file: ''))." + - "To avoid the C(Please explicitly state intended protocol) error, use the I(login_unix_socket) argument, + for example, C(login_unix_socket: /run/mysqld/mysqld.sock)." + - Alternatively, to avoid using I(login_unix_socket) argument on each invocation you can specify the socket path + using the `socket` option in your MySQL config file (usually C(~/.my.cnf)) on the destination host, for + example C(socket=/var/lib/mysql/mysql.sock). +''' diff --git a/ansible_collections/community/mysql/plugins/module_utils/_version.py b/ansible_collections/community/mysql/plugins/module_utils/_version.py new file mode 100644 index 000000000..ce027171c --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/_version.py @@ -0,0 +1,343 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0) +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = '.'.join(map(str, self.version[0:2])) + else: + vstring = '.'.join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + +# end class LooseVersion diff --git a/ansible_collections/community/mysql/plugins/module_utils/database.py b/ansible_collections/community/mysql/plugins/module_utils/database.py new file mode 100644 index 000000000..da0375d5d --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/database.py @@ -0,0 +1,189 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + + +# Input patterns for is_input_dangerous function: +# +# 1. '"' in string and '--' in string or +# "'" in string and '--' in string +PATTERN_1 = re.compile(r'(\'|\").*--') + +# 2. union \ intersect \ except + select +PATTERN_2 = re.compile(r'(UNION|INTERSECT|EXCEPT).*SELECT', re.IGNORECASE) + +# 3. ';' and any KEY_WORDS +PATTERN_3 = re.compile(r';.*(SELECT|UPDATE|INSERT|DELETE|DROP|TRUNCATE|ALTER)', re.IGNORECASE) + + +class SQLParseError(Exception): + pass + + +class UnclosedQuoteError(SQLParseError): + pass + + +# maps a type of identifier to the maximum number of dot levels that are +# allowed to specify that identifier. For example, a database column can be +# specified by up to 4 levels: database.schema.table.column +_PG_IDENTIFIER_TO_DOT_LEVEL = dict( + database=1, + schema=2, + table=3, + column=4, + role=1, + tablespace=1, + sequence=3, + publication=1, +) +_MYSQL_IDENTIFIER_TO_DOT_LEVEL = dict(database=1, table=2, column=3, role=1, vars=1) + + +def _find_end_quote(identifier, quote_char): + accumulate = 0 + while True: + try: + quote = identifier.index(quote_char) + except ValueError: + raise UnclosedQuoteError + accumulate = accumulate + quote + try: + next_char = identifier[quote + 1] + except IndexError: + return accumulate + if next_char == quote_char: + try: + identifier = identifier[quote + 2:] + accumulate = accumulate + 2 + except IndexError: + raise UnclosedQuoteError + else: + return accumulate + + +def _identifier_parse(identifier, quote_char): + if not identifier: + raise SQLParseError('Identifier name unspecified or unquoted trailing dot') + + already_quoted = False + if identifier.startswith(quote_char): + already_quoted = True + try: + end_quote = _find_end_quote(identifier[1:], quote_char=quote_char) + 1 + except UnclosedQuoteError: + already_quoted = False + else: + if end_quote < len(identifier) - 1: + if identifier[end_quote + 1] == '.': + dot = end_quote + 1 + first_identifier = identifier[:dot] + next_identifier = identifier[dot + 1:] + further_identifiers = _identifier_parse(next_identifier, quote_char) + further_identifiers.insert(0, first_identifier) + else: + raise SQLParseError('User escaped identifiers must escape extra quotes') + else: + further_identifiers = [identifier] + + if not already_quoted: + try: + dot = identifier.index('.') + except ValueError: + identifier = identifier.replace(quote_char, quote_char * 2) + identifier = ''.join((quote_char, identifier, quote_char)) + further_identifiers = [identifier] + else: + if dot == 0 or dot >= len(identifier) - 1: + identifier = identifier.replace(quote_char, quote_char * 2) + identifier = ''.join((quote_char, identifier, quote_char)) + further_identifiers = [identifier] + else: + first_identifier = identifier[:dot] + next_identifier = identifier[dot + 1:] + further_identifiers = _identifier_parse(next_identifier, quote_char) + first_identifier = first_identifier.replace(quote_char, quote_char * 2) + first_identifier = ''.join((quote_char, first_identifier, quote_char)) + further_identifiers.insert(0, first_identifier) + + return further_identifiers + + +def pg_quote_identifier(identifier, id_type): + identifier_fragments = _identifier_parse(identifier, quote_char='"') + if len(identifier_fragments) > _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]: + raise SQLParseError('PostgreSQL does not support %s with more than %i dots' % (id_type, _PG_IDENTIFIER_TO_DOT_LEVEL[id_type])) + return '.'.join(identifier_fragments) + + +def mysql_quote_identifier(identifier, id_type): + identifier_fragments = _identifier_parse(identifier, quote_char='`') + if (len(identifier_fragments) - 1) > _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]: + raise SQLParseError('MySQL does not support %s with more than %i dots' % (id_type, _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type])) + + special_cased_fragments = [] + for fragment in identifier_fragments: + if fragment == '`*`': + special_cased_fragments.append('*') + else: + special_cased_fragments.append(fragment) + + return '.'.join(special_cased_fragments) + + +def is_input_dangerous(string): + """Check if the passed string is potentially dangerous. + Can be used to prevent SQL injections. + + Note: use this function only when you can't use + psycopg2's cursor.execute method parametrized + (typically with DDL queries). + """ + if not string: + return False + + for pattern in (PATTERN_1, PATTERN_2, PATTERN_3): + if re.search(pattern, string): + return True + + return False + + +def check_input(module, *args): + """Wrapper for is_input_dangerous function.""" + needs_to_check = args + + dangerous_elements = [] + + for elem in needs_to_check: + if isinstance(elem, str): + if is_input_dangerous(elem): + dangerous_elements.append(elem) + + elif isinstance(elem, list): + for e in elem: + if is_input_dangerous(e): + dangerous_elements.append(e) + + elif elem is None or isinstance(elem, bool): + pass + + else: + elem = str(elem) + if is_input_dangerous(elem): + dangerous_elements.append(elem) + + if dangerous_elements: + module.fail_json(msg="Passed input '%s' is " + "potentially dangerous" % ', '.join(dangerous_elements)) diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/replication.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/replication.py new file mode 100644 index 000000000..a1733e789 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/replication.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion + + +def uses_replica_terminology(cursor): + """Checks if REPLICA must be used instead of SLAVE""" + return LooseVersion(get_server_version(cursor)) >= LooseVersion('10.5.1') diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/role.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/role.py new file mode 100644 index 000000000..d227d598f --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/role.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version + + +def supports_roles(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion('10.0.5') + + +def is_mariadb(): + return True diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py new file mode 100644 index 000000000..c1d2b6133 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version + + +def use_old_user_mgmt(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) < LooseVersion("10.2") + + +def supports_identified_by_password(cursor): + return True + + +def server_supports_alter_user(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("10.2") diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/replication.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/replication.py new file mode 100644 index 000000000..2e50beaf3 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/replication.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion + + +def uses_replica_terminology(cursor): + """Checks if REPLICA must be used instead of SLAVE""" + return LooseVersion(get_server_version(cursor)) >= LooseVersion('8.0.22') diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/role.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/role.py new file mode 100644 index 000000000..932d74a9a --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/role.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version + + +def supports_roles(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion('8') + + +def is_mariadb(): + return False diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py new file mode 100644 index 000000000..1bdad5740 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version + + +def use_old_user_mgmt(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) < LooseVersion("5.7") + + +def supports_identified_by_password(cursor): + version = get_server_version(cursor) + return LooseVersion(version) < LooseVersion("8") + + +def server_supports_alter_user(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("5.6") diff --git a/ansible_collections/community/mysql/plugins/module_utils/mysql.py b/ansible_collections/community/mysql/plugins/module_utils/mysql.py new file mode 100644 index 000000000..b95d20d0d --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/mysql.py @@ -0,0 +1,217 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c), Jonathan Mainguy <jon@soh.re>, 2015 +# Most of this was originally added by Sven Schliesing @muffl0n in the mysql_user.py module +# +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import (absolute_import, division, print_function) +from functools import reduce +__metaclass__ = type + +import os + +from ansible.module_utils.six.moves import configparser +from ansible.module_utils._text import to_native + +try: + import pymysql as mysql_driver + _mysql_cursor_param = 'cursor' +except ImportError: + try: + # mysqlclient is called MySQLdb + import MySQLdb as mysql_driver + import MySQLdb.cursors + _mysql_cursor_param = 'cursorclass' + except ImportError: + mysql_driver = None + +mysql_driver_fail_msg = ('A MySQL module is required: for Python 2.7 either PyMySQL, or ' + 'MySQL-python, or for Python 3.X mysqlclient or PyMySQL. ' + 'Consider setting ansible_python_interpreter to use ' + 'the intended Python version.') + +from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier + + +def get_connector_name(connector): + """ (class) -> str + Return the name of the connector (pymysql or mysqlclient (MySQLdb)) + or 'Unknown' if not pymysql or MySQLdb. When adding a + connector here, also modify get_connector_version. + """ + if connector is None or not hasattr(connector, '__name__'): + return 'Unknown' + + return connector.__name__ + + +def get_connector_version(connector): + """ (class) -> str + Return the version of pymysql or mysqlclient (MySQLdb). + Return 'Unknown' if the connector name is unknown. + """ + + if connector is None: + return 'Unknown' + + connector_name = get_connector_name(connector) + + if connector_name == 'pymysql': + # pymysql has two methods: + # - __version__ that returns the string: 0.7.11.None + # - VERSION that returns the tuple (0, 7, 11, None) + v = connector.VERSION[:3] + return '.'.join(map(str, v)) + elif connector_name == 'MySQLdb': + # version_info returns the tuple (2, 1, 1, 'final', 0) + v = connector.version_info[:3] + return '.'.join(map(str, v)) + else: + return 'Unknown' + + +def parse_from_mysql_config_file(cnf): + # Default values of comment_prefix is '#' and ';'. + # '!' added to prevent a parsing error + # when a config file contains !includedir parameter. + cp = configparser.ConfigParser(comment_prefixes=('#', ';', '!')) + cp.read(cnf) + return cp + + +def mysql_connect(module, login_user=None, login_password=None, config_file='', ssl_cert=None, + ssl_key=None, ssl_ca=None, db=None, cursor_class=None, connect_timeout=30, + autocommit=False, config_overrides_defaults=False, check_hostname=None): + config = {} + + if config_file and os.path.exists(config_file): + config['read_default_file'] = config_file + + if config_overrides_defaults: + try: + cp = parse_from_mysql_config_file(config_file) + except Exception as e: + module.fail_json(msg="Failed to parse %s: %s" % (config_file, to_native(e))) + + # Override some commond defaults with values from config file if needed + if cp and cp.has_section('client'): + try: + module.params['login_host'] = cp.get('client', 'host', fallback=module.params['login_host']) + module.params['login_port'] = cp.getint('client', 'port', fallback=module.params['login_port']) + except Exception as e: + if "got an unexpected keyword argument 'fallback'" in e.message: + module.fail_json(msg='To use config_overrides_defaults, ' + 'it needs Python 3.5+ as the default interpreter on a target host') + + if ssl_ca is not None or ssl_key is not None or ssl_cert is not None or check_hostname is not None: + config['ssl'] = {} + + if module.params['login_unix_socket']: + config['unix_socket'] = module.params['login_unix_socket'] + else: + config['host'] = module.params['login_host'] + config['port'] = module.params['login_port'] + + # If login_user or login_password are given, they should override the + # config file + if login_user is not None: + config['user'] = login_user + if login_password is not None: + config['password'] = login_password + if ssl_cert is not None: + config['ssl']['cert'] = ssl_cert + if ssl_key is not None: + config['ssl']['key'] = ssl_key + if ssl_ca is not None: + config['ssl']['ca'] = ssl_ca + if db is not None: + config['database'] = db + if connect_timeout is not None: + config['connect_timeout'] = connect_timeout + if check_hostname is not None: + if get_connector_name(mysql_driver) == 'pymysql': + version_tuple = (n for n in mysql_driver.__version__.split('.') if n != 'None') + if reduce(lambda x, y: int(x) * 100 + int(y), version_tuple) >= 711: + config['ssl']['check_hostname'] = check_hostname + else: + module.fail_json(msg='To use check_hostname, pymysql >= 0.7.11 is required on the target host') + + if get_connector_name(mysql_driver) == 'pymysql': + # In case of PyMySQL driver: + if mysql_driver.version_info[0] < 1: + # for PyMySQL < 1.0.0, use 'db' instead of 'database' and 'passwd' instead of 'password' + if 'database' in config: + config['db'] = config['database'] + del config['database'] + if 'password' in config: + config['passwd'] = config['password'] + del config['password'] + db_connection = mysql_driver.connect(autocommit=autocommit, **config) + else: + # In case of MySQLdb driver + if mysql_driver.version_info[0] < 2 or (mysql_driver.version_info[0] == 2 and mysql_driver.version_info[1] < 1): + # for MySQLdb < 2.1.0, use 'db' instead of 'database' and 'passwd' instead of 'password' + if 'database' in config: + config['db'] = config['database'] + del config['database'] + if 'password' in config: + config['passwd'] = config['password'] + del config['password'] + db_connection = mysql_driver.connect(**config) + if autocommit: + db_connection.autocommit(True) + + # Monkey patch the Connection class to close the connection when garbage collected + def _conn_patch(conn_self): + conn_self.close() + db_connection.__class__.__del__ = _conn_patch + # Patched + + if cursor_class == 'DictCursor': + return db_connection.cursor(**{_mysql_cursor_param: mysql_driver.cursors.DictCursor}), db_connection + else: + return db_connection.cursor(), db_connection + + +def mysql_common_argument_spec(): + return dict( + login_user=dict(type='str', default=None), + login_password=dict(type='str', no_log=True), + login_host=dict(type='str', default='localhost'), + login_port=dict(type='int', default=3306), + login_unix_socket=dict(type='str'), + config_file=dict(type='path', default='~/.my.cnf'), + connect_timeout=dict(type='int', default=30), + client_cert=dict(type='path', aliases=['ssl_cert']), + client_key=dict(type='path', aliases=['ssl_key']), + ca_cert=dict(type='path', aliases=['ssl_ca']), + check_hostname=dict(type='bool', default=None), + ) + + +def get_server_version(cursor): + """Returns a string representation of the server version.""" + cursor.execute("SELECT VERSION() AS version") + result = cursor.fetchone() + + if isinstance(result, dict): + version_str = result['version'] + else: + version_str = result[0] + + return version_str + + +def set_session_vars(module, cursor, session_vars): + """Set session vars.""" + for var, value in session_vars.items(): + query = "SET SESSION %s = " % mysql_quote_identifier(var, 'vars') + try: + cursor.execute(query + "%s", (value,)) + except Exception as e: + module.fail_json(msg='Failed to execute %s%s: %s' % (query, value, e)) diff --git a/ansible_collections/community/mysql/plugins/module_utils/user.py b/ansible_collections/community/mysql/plugins/module_utils/user.py new file mode 100644 index 000000000..a63ad89b5 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/user.py @@ -0,0 +1,883 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +import string +import re + +from ansible.module_utils.six import iteritems + +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_driver, +) + + +class InvalidPrivsError(Exception): + pass + + +def get_mode(cursor): + cursor.execute('SELECT @@GLOBAL.sql_mode') + result = cursor.fetchone() + mode_str = result[0] + if 'ANSI' in mode_str: + mode = 'ANSI' + else: + mode = 'NOTANSI' + return mode + + +def user_exists(cursor, user, host, host_all): + if host_all: + cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s", (user,)) + else: + cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s", (user, host)) + + count = cursor.fetchone() + return count[0] > 0 + + +def sanitize_requires(tls_requires): + sanitized_requires = {} + if tls_requires: + for key in tls_requires.keys(): + sanitized_requires[key.upper()] = tls_requires[key] + if any(key in ["CIPHER", "ISSUER", "SUBJECT"] for key in sanitized_requires.keys()): + sanitized_requires.pop("SSL", None) + sanitized_requires.pop("X509", None) + return sanitized_requires + + if "X509" in sanitized_requires.keys(): + sanitized_requires = "X509" + else: + sanitized_requires = "SSL" + + return sanitized_requires + return None + + +def mogrify_requires(query, params, tls_requires): + if tls_requires: + if isinstance(tls_requires, dict): + k, v = zip(*tls_requires.items()) + requires_query = " AND ".join(("%s %%s" % key for key in k)) + params += v + else: + requires_query = tls_requires + query = " REQUIRE ".join((query, requires_query)) + return query, params + + +def do_not_mogrify_requires(query, params, tls_requires): + return query, params + + +def get_tls_requires(cursor, user, host): + if user: + if not impl.use_old_user_mgmt(cursor): + query = "SHOW CREATE USER '%s'@'%s'" % (user, host) + else: + query = "SHOW GRANTS for '%s'@'%s'" % (user, host) + + cursor.execute(query) + require_list = [tuple[0] for tuple in filter(lambda x: "REQUIRE" in x[0], cursor.fetchall())] + require_line = require_list[0] if require_list else "" + pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" + requires_match = re.search(pattern, require_line) + requires = requires_match.group().strip() if requires_match else "" + if any((requires.startswith(req) for req in ('SSL', 'X509', 'NONE'))): + requires = requires.split()[0] + if requires == 'NONE': + requires = None + else: + import shlex + + items = iter(shlex.split(requires)) + requires = dict(zip(items, items)) + return requires or None + + +def get_grants(cursor, user, host): + cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) + grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0] + pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))" + grants = re.search(pattern, grants_line[0]).group().strip() + return grants.split(", ") + + +def get_existing_authentication(cursor, user): + # Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string. + cursor.execute("SELECT VERSION()") + if 'mariadb' in cursor.fetchone()[0].lower(): + # before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ + # when using mysql_native_password + cursor.execute("""select plugin, auth from ( + select plugin, password as auth from mysql.user where user=%(user)s + union select plugin, authentication_string as auth from mysql.user where user=%(user)s + ) x group by plugin, auth limit 2 + """, {'user': user}) + else: + cursor.execute("""select plugin, authentication_string as auth from mysql.user where user=%(user)s + group by plugin, authentication_string limit 2""", {'user': user}) + rows = cursor.fetchall() + if len(rows) == 1: + return {'plugin': rows[0][0], 'auth_string': rows[0][1]} + return None + + +def user_add(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, new_priv, + tls_requires, check_mode, reuse_existing_password): + # we cannot create users without a proper hostname + if host_all: + return {'changed': False, 'password_changed': False} + + if check_mode: + return {'changed': True, 'password_changed': None} + + # Determine what user management method server uses + old_user_mgmt = impl.use_old_user_mgmt(cursor) + + mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires + + used_existing_password = False + if reuse_existing_password: + existing_auth = get_existing_authentication(cursor, user) + if existing_auth: + plugin = existing_auth['plugin'] + plugin_hash_string = existing_auth['auth_string'] + password = None + used_existing_password = True + if password and encrypted: + if impl.supports_identified_by_password(cursor): + query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password) + else: + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, password) + elif password and not encrypted: + if old_user_mgmt: + query_with_args = "CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password) + else: + cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) + encrypted_password = cursor.fetchone()[0] + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password) + elif plugin and plugin_hash_string: + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string) + elif plugin and plugin_auth_string: + # Mysql and MariaDB differ in naming pam plugin and Syntax to set it + if plugin == 'pam': # Used by MariaDB which requires the USING keyword, not BY + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string) + else: + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) + elif plugin: + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin) + else: + query_with_args = "CREATE USER %s@%s", (user, host) + + query_with_args_and_tls_requires = query_with_args + (tls_requires,) + cursor.execute(*mogrify(*query_with_args_and_tls_requires)) + + if new_priv is not None: + for db_table, priv in iteritems(new_priv): + privileges_grant(cursor, user, host, db_table, priv, tls_requires) + if tls_requires is not None: + privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) + return {'changed': True, 'password_changed': not used_existing_password} + + +def is_hash(password): + ishash = False + if len(password) == 41 and password[0] == '*': + if frozenset(password[1:]).issubset(string.hexdigits): + ishash = True + return ishash + + +def user_mod(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, new_priv, + append_privs, subtract_privs, tls_requires, module, role=False, maria_role=False): + changed = False + msg = "User unchanged" + grant_option = False + + # Determine what user management method server uses + old_user_mgmt = impl.use_old_user_mgmt(cursor) + + if host_all and not role: + hostnames = user_get_hostnames(cursor, user) + else: + hostnames = [host] + + password_changed = False + for host in hostnames: + # Handle clear text and hashed passwords. + if not role: + if bool(password): + + # Get a list of valid columns in mysql.user table to check if Password and/or authentication_string exist + cursor.execute(""" + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') + ORDER BY COLUMN_NAME DESC LIMIT 1 + """) + colA = cursor.fetchone() + + cursor.execute(""" + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') + ORDER BY COLUMN_NAME ASC LIMIT 1 + """) + colB = cursor.fetchone() + + # Select hash from either Password or authentication_string, depending which one exists and/or is filled + cursor.execute(""" + SELECT COALESCE( + CASE WHEN %s = '' THEN NULL ELSE %s END, + CASE WHEN %s = '' THEN NULL ELSE %s END + ) + FROM mysql.user WHERE user = %%s AND host = %%s + """ % (colA[0], colA[0], colB[0], colB[0]), (user, host)) + current_pass_hash = cursor.fetchone()[0] + if isinstance(current_pass_hash, bytes): + current_pass_hash = current_pass_hash.decode('ascii') + + if encrypted: + encrypted_password = password + if not is_hash(encrypted_password): + module.fail_json(msg="encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))") + else: + if old_user_mgmt: + cursor.execute("SELECT PASSWORD(%s)", (password,)) + else: + cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) + encrypted_password = cursor.fetchone()[0] + + if current_pass_hash != encrypted_password: + password_changed = True + msg = "Password updated" + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + if old_user_mgmt: + cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) + msg = "Password updated (old style)" + else: + try: + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)) + msg = "Password updated (new style)" + except (mysql_driver.Error) as e: + # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql + # Replacing empty root password with new authentication mechanisms fails with error 1396 + if e.args[0] == 1396: + cursor.execute( + "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", + ('mysql_native_password', encrypted_password, user, host) + ) + cursor.execute("FLUSH PRIVILEGES") + msg = "Password forced update" + else: + raise e + changed = True + + # Handle plugin authentication + if plugin and not role: + cursor.execute("SELECT plugin, authentication_string FROM mysql.user " + "WHERE user = %s AND host = %s", (user, host)) + current_plugin = cursor.fetchone() + + update = False + + if current_plugin[0] != plugin: + update = True + + if plugin_hash_string and current_plugin[1] != plugin_hash_string: + update = True + + if plugin_auth_string and current_plugin[1] != plugin_auth_string: + # this case can cause more updates than expected, + # as plugin can hash auth_string in any way it wants + # and there's no way to figure it out for + # a check, so I prefer to update more often than never + update = True + + if update: + if plugin_hash_string: + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string) + elif plugin_auth_string: + # Mysql and MariaDB differ in naming pam plugin and syntax to set it + if plugin == 'pam': + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string) + else: + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) + else: + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin) + + cursor.execute(*query_with_args) + password_changed = True + changed = True + + # Handle privileges + if new_priv is not None: + curr_priv = privileges_get(cursor, user, host, maria_role) + + # If the user has privileges on a db.table that doesn't appear at all in + # the new specification, then revoke all privileges on it. + if not append_privs and not subtract_privs: + for db_table, priv in iteritems(curr_priv): + # If the user has the GRANT OPTION on a db.table, revoke it first. + if "GRANT" in priv: + grant_option = True + if db_table not in new_priv: + if user != "root" and "PROXY" not in priv: + msg = "Privileges updated" + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) + changed = True + + # If the user doesn't currently have any privileges on a db.table, then + # we can perform a straight grant operation. + if not subtract_privs: + for db_table, priv in iteritems(new_priv): + if db_table not in curr_priv: + msg = "New privileges granted" + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) + changed = True + + # If the db.table specification exists in both the user's current privileges + # and in the new privileges, then we need to see if there's a difference. + db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) + for db_table in db_table_intersect: + + grant_privs = [] + revoke_privs = [] + if append_privs: + # When appending privileges, only missing privileges need to be granted. Nothing is revoked. + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + elif subtract_privs: + # When subtracting privileges, revoke only the intersection of requested and current privileges. + # No privileges are granted. + revoke_privs = list(set(new_priv[db_table]) & set(curr_priv[db_table])) + else: + # When replacing (neither append_privs nor subtract_privs), grant all missing privileges + # and revoke existing privileges that were not requested... + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + revoke_privs = list(set(curr_priv[db_table]) - set(new_priv[db_table])) + + # ... avoiding pointless revocations when ALL are granted + if 'ALL' in grant_privs or 'ALL PRIVILEGES' in grant_privs: + revoke_privs = list(set(['GRANT', 'PROXY']).intersection(set(revoke_privs))) + + # Only revoke grant option if it exists and absence is requested + # + # For more details + # https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807 + grant_option = 'GRANT' in revoke_privs and 'GRANT' not in grant_privs + + if grant_privs == ['GRANT']: + # USAGE grants no privileges, it is only needed because 'WITH GRANT OPTION' cannot stand alone + grant_privs.append('USAGE') + + if len(grant_privs) + len(revoke_privs) > 0: + msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs) + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + if len(revoke_privs) > 0: + privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) + if len(grant_privs) > 0: + privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) + + # after privilege manipulation, compare privileges from before and now + after_priv = privileges_get(cursor, user, host, maria_role) + changed = changed or (curr_priv != after_priv) + + if role: + continue + + # Handle TLS requirements + current_requires = get_tls_requires(cursor, user, host) + if current_requires != tls_requires: + msg = "TLS requires updated" + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + if not old_user_mgmt: + pre_query = "ALTER USER" + else: + pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host)) + + if tls_requires is not None: + query = " ".join((pre_query, "%s@%s")) + query_with_args = mogrify_requires(query, (user, host), tls_requires) + else: + query = " ".join((pre_query, "%s@%s REQUIRE NONE")) + query_with_args = query, (user, host) + + cursor.execute(*query_with_args) + changed = True + + return {'changed': changed, 'msg': msg, 'password_changed': password_changed} + + +def user_delete(cursor, user, host, host_all, check_mode): + if check_mode: + return True + + if host_all: + hostnames = user_get_hostnames(cursor, user) + else: + hostnames = [host] + + for hostname in hostnames: + try: + cursor.execute("DROP USER IF EXISTS %s@%s", (user, hostname)) + except Exception: + cursor.execute("DROP USER %s@%s", (user, hostname)) + + return True + + +def user_get_hostnames(cursor, user): + cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", (user,)) + hostnames_raw = cursor.fetchall() + hostnames = [] + + for hostname_raw in hostnames_raw: + hostnames.append(hostname_raw[0]) + + return hostnames + + +def privileges_get(cursor, user, host, maria_role=False): + """ MySQL doesn't have a better method of getting privileges aside from the + SHOW GRANTS query syntax, which requires us to then parse the returned string. + Here's an example of the string that is returned from MySQL: + + GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass'; + + This function makes the query and returns a dictionary containing the results. + The dictionary format is the same as that returned by privileges_unpack() below. + """ + output = {} + if not maria_role: + cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) + else: + cursor.execute("SHOW GRANTS FOR %s", (user,)) + grants = cursor.fetchall() + + def pick(x): + if x == 'ALL PRIVILEGES': + return 'ALL' + else: + return x + + for grant in grants: + if not maria_role: + res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0]) + else: + res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3""", grant[0]) + + if res is None: + # If a user has roles assigned, we'll have one of priv tuples looking like + # GRANT `admin`@`%` TO `user1`@`localhost` + # which will result None as res value. + # As we use the mysql_role module to manipulate roles + # we just ignore such privs below: + res = re.match("""GRANT (.+) TO (['`"]).*""", grant[0]) + if not maria_role and res: + continue + + raise InvalidPrivsError('unable to parse the MySQL grant string: %s' % grant[0]) + + privileges = res.group(1).split(",") + privileges = [pick(x.strip()) for x in privileges] + + # Handle cases when there's privs like GRANT SELECT (colA, ...) in privs. + # To this point, the privileges list can look like + # ['SELECT (`A`', '`B`)', 'INSERT'] that is incorrect (SELECT statement is splitted). + # Columns should also be sorted to compare it with desired privileges later. + # Determine if there's a case similar to the above: + privileges = normalize_col_grants(privileges) + + if not maria_role: + if "WITH GRANT OPTION" in res.group(7): + privileges.append('GRANT') + db = res.group(2) + output.setdefault(db, []).extend(privileges) + return output + + +def normalize_col_grants(privileges): + """Fix and sort grants on columns in privileges list + + Make ['SELECT (A, B)', 'INSERT (A, B)', 'DETELE'] + from ['SELECT (A', 'B)', 'INSERT (B', 'A)', 'DELETE']. + See unit tests in tests/unit/plugins/modules/test_mysql_user.py + """ + for grant in ('SELECT', 'UPDATE', 'INSERT', 'REFERENCES'): + start, end = has_grant_on_col(privileges, grant) + # If not, either start and end will be None + if start is not None: + privileges = handle_grant_on_col(privileges, start, end) + + return privileges + + +def has_grant_on_col(privileges, grant): + """Check if there is a statement like SELECT (colA, colB) + in the privilege list. + + Return (start index, end index). + """ + # Determine elements of privileges where + # columns are listed + start = None + end = None + for n, priv in enumerate(privileges): + if '%s (' % grant in priv: + # We found the start element + start = n + + if start is not None and ')' in priv: + # We found the end element + end = n + break + + if start is not None and end is not None: + # if the privileges list consist of, for example, + # ['SELECT (A', 'B), 'INSERT'], return indexes of related elements + return start, end + else: + # If start and end position is the same element, + # it means there's expression like 'SELECT (A)', + # so no need to handle it + return None, None + + +def handle_grant_on_col(privileges, start, end): + """Handle cases when the privs like SELECT (colA, ...) is in the privileges list.""" + # When the privileges list look like ['SELECT (colA,', 'colB)'] + # (Notice that the statement is splitted) + if start != end: + output = list(privileges[:start]) + + select_on_col = ', '.join(privileges[start:end + 1]) + + select_on_col = sort_column_order(select_on_col) + + output.append(select_on_col) + + output.extend(privileges[end + 1:]) + + # When it look like it should be, e.g. ['SELECT (colA, colB)'], + # we need to be sure, the columns is sorted + else: + output = list(privileges) + output[start] = sort_column_order(output[start]) + + return output + + +def sort_column_order(statement): + """Sort column order in grants like SELECT (colA, colB, ...). + + MySQL changes columns order like below: + --------------------------------------- + mysql> GRANT SELECT (testColA, testColB), INSERT ON `testDb`.`testTable` TO 'testUser'@'localhost'; + Query OK, 0 rows affected (0.04 sec) + + mysql> flush privileges; + Query OK, 0 rows affected (0.00 sec) + + mysql> SHOW GRANTS FOR testUser@localhost; + +---------------------------------------------------------------------------------------------+ + | Grants for testUser@localhost | + +---------------------------------------------------------------------------------------------+ + | GRANT USAGE ON *.* TO 'testUser'@'localhost' | + | GRANT SELECT (testColB, testColA), INSERT ON `testDb`.`testTable` TO 'testUser'@'localhost' | + +---------------------------------------------------------------------------------------------+ + + We should sort columns in our statement, otherwise the module always will return + that the state has changed. + """ + # 1. Extract stuff inside () + # 2. Split + # 3. Sort + # 4. Put between () and return + + # "SELECT/UPDATE/.. (colA, colB) => "colA, colB" + tmp = statement.split('(') + priv_name = tmp[0] + columns = tmp[1].rstrip(')') + + # "colA, colB" => ["colA", "colB"] + columns = columns.split(',') + + for i, col in enumerate(columns): + col = col.strip() + columns[i] = col.strip('`') + + columns.sort() + return '%s(%s)' % (priv_name, ', '.join(columns)) + + +def privileges_unpack(priv, mode, ensure_usage=True): + """ Take a privileges string, typically passed as a parameter, and unserialize + it into a dictionary, the same format as privileges_get() above. We have this + custom format to avoid using YAML/JSON strings inside YAML playbooks. Example + of a privileges string: + + mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL + + The privilege USAGE stands for no privileges, so we add that in on *.* if it's + not specified in the string, as MySQL will always provide this by default. + """ + if mode == 'ANSI': + quote = '"' + else: + quote = '`' + output = {} + privs = [] + for item in priv.strip().split('/'): + pieces = item.strip().rsplit(':', 1) + dbpriv = pieces[0].rsplit(".", 1) + + # Check for FUNCTION or PROCEDURE object types + parts = dbpriv[0].split(" ", 1) + object_type = '' + if len(parts) > 1 and (parts[0] == 'FUNCTION' or parts[0] == 'PROCEDURE'): + object_type = parts[0] + ' ' + dbpriv[0] = parts[1] + + # Do not escape if privilege is for database or table, i.e. + # neither quote *. nor .* + for i, side in enumerate(dbpriv): + if side.strip('`') != '*': + dbpriv[i] = '%s%s%s' % (quote, side.strip('`'), quote) + pieces[0] = object_type + '.'.join(dbpriv) + + if '(' in pieces[1]: + output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper()) + for i in output[pieces[0]]: + privs.append(re.sub(r'\s*\(.*\)', '', i)) + else: + output[pieces[0]] = pieces[1].upper().split(',') + privs = output[pieces[0]] + + # Handle cases when there's privs like GRANT SELECT (colA, ...) in privs. + output[pieces[0]] = normalize_col_grants(output[pieces[0]]) + + if ensure_usage and '*.*' not in output: + output['*.*'] = ['USAGE'] + + return output + + +def privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role=False): + # Escape '%' since mysql db.execute() uses a format string + db_table = db_table.replace('%', '%%') + if grant_option: + query = ["REVOKE GRANT OPTION ON %s" % db_table] + if not maria_role: + query.append("FROM %s@%s") + else: + query.append("FROM %s") + + query = ' '.join(query) + cursor.execute(query, (user, host)) + priv_string = ",".join([p for p in priv if p not in ('GRANT', )]) + + if priv_string != "": + query = ["REVOKE %s ON %s" % (priv_string, db_table)] + + if not maria_role: + query.append("FROM %s@%s") + params = (user, host) + else: + query.append("FROM %s") + params = (user,) + + query = ' '.join(query) + cursor.execute(query, params) + cursor.execute("FLUSH PRIVILEGES") + + +def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role=False): + # Escape '%' since mysql db.execute uses a format string and the + # specification of db and table often use a % (SQL wildcard) + db_table = db_table.replace('%', '%%') + priv_string = ",".join([p for p in priv if p not in ('GRANT', )]) + query = ["GRANT %s ON %s" % (priv_string, db_table)] + + if not maria_role: + query.append("TO %s@%s") + params = (user, host) + else: + query.append("TO %s") + params = (user) + + if tls_requires and impl.use_old_user_mgmt(cursor): + query, params = mogrify_requires(" ".join(query), params, tls_requires) + query = [query] + if 'GRANT' in priv: + query.append("WITH GRANT OPTION") + query = ' '.join(query) + + if isinstance(params, str): + params = (params,) + + try: + cursor.execute(query, params) + except (mysql_driver.ProgrammingError, mysql_driver.OperationalError, mysql_driver.InternalError) as e: + raise InvalidPrivsError("Error granting privileges, invalid priv string: %s , params: %s, query: %s ," + " exception: %s." % (priv_string, str(params), query, str(e))) + + +def convert_priv_dict_to_str(priv): + """Converts privs dictionary to string of certain format. + + Args: + priv (dict): Dict of privileges that needs to be converted to string. + + Returns: + priv (str): String representation of input argument. + """ + priv_list = ['%s:%s' % (key, val) for key, val in iteritems(priv)] + + return '/'.join(priv_list) + + +def get_resource_limits(cursor, user, host): + """Get user resource limits. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + + Returns: Dictionary containing current resource limits. + """ + + query = ('SELECT max_questions AS MAX_QUERIES_PER_HOUR, ' + 'max_updates AS MAX_UPDATES_PER_HOUR, ' + 'max_connections AS MAX_CONNECTIONS_PER_HOUR, ' + 'max_user_connections AS MAX_USER_CONNECTIONS ' + 'FROM mysql.user WHERE User = %s AND Host = %s') + cursor.execute(query, (user, host)) + res = cursor.fetchone() + + if not res: + return None + + current_limits = { + 'MAX_QUERIES_PER_HOUR': res[0], + 'MAX_UPDATES_PER_HOUR': res[1], + 'MAX_CONNECTIONS_PER_HOUR': res[2], + 'MAX_USER_CONNECTIONS': res[3], + } + + cursor.execute("SELECT VERSION()") + if 'mariadb' in cursor.fetchone()[0].lower(): + query = ('SELECT max_statement_time AS MAX_STATEMENT_TIME ' + 'FROM mysql.user WHERE User = %s AND Host = %s') + cursor.execute(query, (user, host)) + res_max_statement_time = cursor.fetchone() + current_limits['MAX_STATEMENT_TIME'] = res_max_statement_time[0] + + return current_limits + + +def match_resource_limits(module, current, desired): + """Check and match limits. + + Args: + module (AnsibleModule): Ansible module object. + current (dict): Dictionary with current limits. + desired (dict): Dictionary with desired limits. + + Returns: Dictionary containing parameters that need to change. + """ + + if not current: + # It means the user does not exists, so we need + # to set all limits after its creation + return desired + + needs_to_change = {} + + for key, val in iteritems(desired): + if key not in current: + # Supported keys are listed in the documentation + # and must be determined in the get_resource_limits function + # (follow 'AS' keyword) + module.fail_json(msg="resource_limits: key '%s' is unsupported." % key) + + try: + val = int(val) + except Exception: + module.fail_json(msg="Can't convert value '%s' to integer." % val) + + if val != current.get(key): + needs_to_change[key] = val + + return needs_to_change + + +def limit_resources(module, cursor, user, host, resource_limits, check_mode): + """Limit user resources. + + Args: + module (AnsibleModule): Ansible module object. + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + resource_limit (dict): Dictionary with desired limits. + check_mode (bool): Run the function in check mode or not. + + Returns: True, if changed, False otherwise. + """ + if not impl.server_supports_alter_user(cursor): + module.fail_json(msg="The server version does not match the requirements " + "for resource_limits parameter. See module's documentation.") + + cursor.execute("SELECT VERSION()") + if 'mariadb' not in cursor.fetchone()[0].lower(): + if 'MAX_STATEMENT_TIME' in resource_limits: + module.fail_json(msg="MAX_STATEMENT_TIME resource limit is only supported by MariaDB.") + + current_limits = get_resource_limits(cursor, user, host) + + needs_to_change = match_resource_limits(module, current_limits, resource_limits) + + if not needs_to_change: + return False + + if needs_to_change and check_mode: + return True + + # If not check_mode + tmp = [] + for key, val in iteritems(needs_to_change): + tmp.append('%s %s' % (key, val)) + + query = "ALTER USER %s@%s" + query += ' WITH %s' % ' '.join(tmp) + cursor.execute(query, (user, host)) + return True + + +def get_impl(cursor): + global impl + cursor.execute("SELECT VERSION()") + if 'mariadb' in cursor.fetchone()[0].lower(): + from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import user as mariauser + impl = mariauser + else: + from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser + impl = mysqluser diff --git a/ansible_collections/community/mysql/plugins/module_utils/version.py b/ansible_collections/community/mysql/plugins/module_utils/version.py new file mode 100644 index 000000000..94731347c --- /dev/null +++ b/ansible_collections/community/mysql/plugins/module_utils/version.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Provide version object to compare version numbers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +# Once we drop support for ansible-core 2.11, we can +# remove the _version.py file, and replace the following import by +# +# from ansible.module_utils.compat.version import LooseVersion + +from ._version import LooseVersion diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_db.py b/ansible_collections/community/mysql/plugins/modules/mysql_db.py new file mode 100644 index 000000000..5a8fe3e3e --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_db.py @@ -0,0 +1,763 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com> +# Sponsored by Four Kitchens http://fourkitchens.com. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_db +short_description: Add or remove MySQL databases from a remote host +description: +- Add or remove MySQL databases from a remote host. +options: + name: + description: + - Name of the database to add or remove. + - I(name=all) may only be provided if I(state) is C(dump) or C(import). + - List of databases is provided with I(state=dump), I(state=present) and I(state=absent). + - If I(name=all) it works like --all-databases option for mysqldump (Added in 2.0). + required: true + type: list + elements: str + aliases: [db] + state: + description: + - The database state. + type: str + default: present + choices: ['absent', 'dump', 'import', 'present'] + collation: + description: + - Collation mode (sorting). This only applies to new table/databases and + does not update existing ones, this is a limitation of MySQL. + type: str + default: '' + encoding: + description: + - Encoding mode to use, examples include C(utf8) or C(latin1_swedish_ci), + at creation of database, dump or importation of sql script. + type: str + default: '' + target: + description: + - Location, on the remote host, of the dump file to read from or write to. + - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and + xz (Added in 2.0) compressed files are supported. + type: path + single_transaction: + description: + - Execute the dump in a single transaction. + type: bool + default: false + quick: + description: + - Option used for dumping large tables. + type: bool + default: true + ignore_tables: + description: + - A list of table names that will be ignored in the dump + of the form database_name.table_name. + type: list + elements: str + default: [] + hex_blob: + description: + - Dump binary columns using hexadecimal notation. + type: bool + default: false + version_added: '0.1.0' + force: + description: + - Continue dump or import even if we get an SQL error. + - Used only when I(state) is C(dump) or C(import). + type: bool + default: false + version_added: '0.1.0' + master_data: + description: + - Option to dump a master replication server to produce a dump file + that can be used to set up another server as a slave of the master. + - C(0) to not include master data. + - C(1) to generate a 'CHANGE MASTER TO' statement + required on the slave to start the replication process. + - C(2) to generate a commented 'CHANGE MASTER TO'. + - Can be used when I(state=dump). + type: int + choices: [0, 1, 2] + default: 0 + version_added: '0.1.0' + skip_lock_tables: + description: + - Skip locking tables for read. Used when I(state=dump), ignored otherwise. + type: bool + default: false + version_added: '0.1.0' + dump_extra_args: + description: + - Provide additional arguments for mysqldump. + Used when I(state=dump) only, ignored otherwise. + type: str + version_added: '0.1.0' + use_shell: + description: + - Used to prevent C(Broken pipe) errors when the imported I(target) file is compressed. + - If C(yes), the module will internally execute commands via a shell. + - Used when I(state=import), ignored otherwise. + type: bool + default: false + version_added: '0.1.0' + unsafe_login_password: + description: + - If C(no), the module will safely use a shell-escaped + version of the I(login_password) value. + - It makes sense to use C(yes) only if there are special + symbols in the value and errors C(Access denied) occur. + - Used only when I(state) is C(import) or C(dump) and + I(login_password) is passed, ignored otherwise. + type: bool + default: false + version_added: '0.1.0' + restrict_config_file: + description: + - Read only passed I(config_file). + - When I(state) is C(dump) or C(import), + by default the module passes I(config_file) parameter + using C(--defaults-extra-file) command-line argument to C(mysql/mysqldump) utilities + under the hood that read named option file in addition to usual option files. + - If this behavior is undesirable, use C(yes) to read only named option file. + type: bool + default: false + version_added: '0.1.0' + check_implicit_admin: + description: + - Check if mysql allows login as root/nopassword before trying supplied credentials. + - If success, passed I(login_user)/I(login_password) will be ignored. + type: bool + default: false + version_added: '0.1.0' + config_overrides_defaults: + description: + - If C(yes), connection parameters from I(config_file) will override the default + values of I(login_host) and I(login_port) parameters. + - Used when I(stat) is C(present) or C(absent), ignored otherwise. + - It needs Python 3.5+ as the default interpreter on a target host. + type: bool + default: false + version_added: '0.1.0' + chdir: + description: + - Changes the current working directory. + - Can be useful, for example, when I(state=import) and a dump file contains relative paths. + type: path + version_added: '3.4.0' + pipefail: + description: + - Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the + mysql_dump command when I(state=import) and compression is used. + - The default is C(no) to prevent issues on systems without bash as a default interpreter. + - The default will change to C(yes) in community.mysql 4.0.0. + type: bool + default: false + version_added: '3.4.0' + +seealso: +- module: community.mysql.mysql_info +- module: community.mysql.mysql_variables +- module: community.mysql.mysql_user +- module: community.mysql.mysql_replication +- name: MySQL command-line client reference + description: Complete reference of the MySQL command-line client documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/mysql.html +- name: mysqldump reference + description: Complete reference of the ``mysqldump`` client utility documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html +- name: CREATE DATABASE reference + description: Complete reference of the CREATE DATABASE command documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/create-database.html +- name: DROP DATABASE reference + description: Complete reference of the DROP DATABASE command documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/drop-database.html +author: "Ansible Core Team" +requirements: + - mysql (command line binary) + - mysqldump (command line binary) +notes: + - Supports C(check_mode). + - Requires the mysql and mysqldump binaries on the remote host. + - This module is B(not idempotent) when I(state) is C(import), + and will import the dump file each time if run more than once. +extends_documentation_fragment: +- community.mysql.mysql + +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Create a new database with name 'bobdata' + community.mysql.mysql_db: + name: bobdata + state: present + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Create new databases with names 'foo' and 'bar' + community.mysql.mysql_db: + name: + - foo + - bar + state: present + +# Copy database dump file to remote host and restore it to database 'my_db' +- name: Copy database dump file + copy: + src: dump.sql.bz2 + dest: /tmp + +- name: Restore database + community.mysql.mysql_db: + name: my_db + state: import + target: /tmp/dump.sql.bz2 + +- name: Restore database ignoring errors + community.mysql.mysql_db: + name: my_db + state: import + target: /tmp/dump.sql.bz2 + force: true + +- name: Dump multiple databases + community.mysql.mysql_db: + state: dump + name: db_1,db_2 + target: /tmp/dump.sql + +- name: Dump multiple databases + community.mysql.mysql_db: + state: dump + name: + - db_1 + - db_2 + target: /tmp/dump.sql + +- name: Dump all databases to hostname.sql + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/dump.sql + +- name: Dump all databases to hostname.sql including master data + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/dump.sql + master_data: 1 + +# Import of sql script with encoding option +- name: > + Import dump.sql with specific latin1 encoding, + similar to mysql -u <username> --default-character-set=latin1 -p <password> < dump.sql + community.mysql.mysql_db: + state: import + name: all + encoding: latin1 + target: /tmp/dump.sql + +# Dump of database with encoding option +- name: > + Dump of Databse with specific latin1 encoding, + similar to mysqldump -u <username> --default-character-set=latin1 -p <password> <database> + community.mysql.mysql_db: + state: dump + name: db_1 + encoding: latin1 + target: /tmp/dump.sql + +- name: Delete database with name 'bobdata' + community.mysql.mysql_db: + name: bobdata + state: absent + +- name: Make sure there is neither a database with name 'foo', nor one with name 'bar' + community.mysql.mysql_db: + name: + - foo + - bar + state: absent + +# Dump database with argument not directly supported by this module +# using dump_extra_args parameter +- name: Dump databases without including triggers + community.mysql.mysql_db: + state: dump + name: foo + target: /tmp/dump.sql + dump_extra_args: --skip-triggers + +- name: Try to create database as root/nopassword first. If not allowed, pass the credentials + community.mysql.mysql_db: + check_implicit_admin: true + login_user: bob + login_password: 123456 + name: bobdata + state: present + +- name: Dump a database with compression and catch errors from mysqldump with bash pipefail + community.mysql.mysql_db: + state: dump + name: foo + target: /tmp/dump.sql.gz + pipefail: true +''' + +RETURN = r''' +db: + description: Database names in string format delimited by white space. + returned: always + type: str + sample: "foo bar" +db_list: + description: List of database names. + returned: always + type: list + sample: ["foo", "bar"] +executed_commands: + description: List of commands which tried to run. + returned: if executed + type: list + sample: ["CREATE DATABASE acme"] + version_added: '0.1.0' +''' + +import os +import subprocess +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier +from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec +from ansible.module_utils.six.moves import shlex_quote +from ansible.module_utils._text import to_native + +executed_commands = [] + +# =========================================== +# MySQL module specific support methods. +# + + +def db_exists(cursor, db): + res = 0 + for each_db in db: + res += cursor.execute("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = %s", (each_db,)) + return res == len(db) + + +def db_delete(cursor, db): + if not db: + return False + for each_db in db: + query = "DROP DATABASE %s" % mysql_quote_identifier(each_db, 'database') + executed_commands.append(query) + cursor.execute(query) + return True + + +def db_dump(module, host, user, password, db_name, target, all_databases, port, + config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, + single_transaction=None, quick=None, ignore_tables=None, hex_blob=None, + encoding=None, force=False, master_data=0, skip_lock_tables=False, + dump_extra_args=None, unsafe_password=False, restrict_config_file=False, + check_implicit_admin=False, pipefail=False): + cmd = module.get_bin_path('mysqldump', True) + # If defined, mysqldump demands --defaults-extra-file be the first option + if config_file: + if restrict_config_file: + cmd += " --defaults-file=%s" % shlex_quote(config_file) + else: + cmd += " --defaults-extra-file=%s" % shlex_quote(config_file) + + if check_implicit_admin: + cmd += " --user=root --password=''" + else: + if user is not None: + cmd += " --user=%s" % shlex_quote(user) + + if password is not None: + if not unsafe_password: + cmd += " --password=%s" % shlex_quote(password) + else: + cmd += " --password=%s" % password + + if ssl_cert is not None: + cmd += " --ssl-cert=%s" % shlex_quote(ssl_cert) + if ssl_key is not None: + cmd += " --ssl-key=%s" % shlex_quote(ssl_key) + if ssl_ca is not None: + cmd += " --ssl-ca=%s" % shlex_quote(ssl_ca) + if force: + cmd += " --force" + if socket is not None: + cmd += " --socket=%s" % shlex_quote(socket) + else: + cmd += " --host=%s --port=%i" % (shlex_quote(host), port) + + if all_databases: + cmd += " --all-databases" + elif len(db_name) > 1: + cmd += " --databases {0}".format(' '.join(db_name)) + else: + cmd += " %s" % shlex_quote(' '.join(db_name)) + + if skip_lock_tables: + cmd += " --skip-lock-tables" + if (encoding is not None) and (encoding != ""): + cmd += " --default-character-set=%s" % shlex_quote(encoding) + if single_transaction: + cmd += " --single-transaction=true" + if quick: + cmd += " --quick" + if ignore_tables: + for an_ignored_table in ignore_tables: + cmd += " --ignore-table={0}".format(an_ignored_table) + if hex_blob: + cmd += " --hex-blob" + if master_data: + cmd += " --master-data=%s" % master_data + if dump_extra_args is not None: + cmd += " " + dump_extra_args + + path = None + if os.path.splitext(target)[-1] == '.gz': + path = module.get_bin_path('gzip', True) + elif os.path.splitext(target)[-1] == '.bz2': + path = module.get_bin_path('bzip2', True) + elif os.path.splitext(target)[-1] == '.xz': + path = module.get_bin_path('xz', True) + + if path: + cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target)) + if pipefail: + cmd = 'set -o pipefail && ' + cmd + else: + cmd += " > %s" % shlex_quote(target) + + executed_commands.append(cmd) + + if pipefail: + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True, executable='bash') + else: + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) + + return rc, stdout, stderr + + +def db_import(module, host, user, password, db_name, target, all_databases, port, config_file, + socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, encoding=None, force=False, + use_shell=False, unsafe_password=False, restrict_config_file=False, + check_implicit_admin=False): + if not os.path.exists(target): + return module.fail_json(msg="target %s does not exist on the host" % target) + + cmd = [module.get_bin_path('mysql', True)] + # --defaults-file must go first, or errors out + if config_file: + if restrict_config_file: + cmd.append("--defaults-file=%s" % shlex_quote(config_file)) + else: + cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file)) + + if check_implicit_admin: + cmd.append("--user=root --password=''") + else: + if user: + cmd.append("--user=%s" % shlex_quote(user)) + + if password: + if not unsafe_password: + cmd.append("--password=%s" % shlex_quote(password)) + else: + cmd.append("--password=%s" % password) + + if ssl_cert is not None: + cmd.append("--ssl-cert=%s" % shlex_quote(ssl_cert)) + if ssl_key is not None: + cmd.append("--ssl-key=%s" % shlex_quote(ssl_key)) + if ssl_ca is not None: + cmd.append("--ssl-ca=%s" % shlex_quote(ssl_ca)) + if force: + cmd.append("-f") + if socket is not None: + cmd.append("--socket=%s" % shlex_quote(socket)) + else: + cmd.append("--host=%s" % shlex_quote(host)) + cmd.append("--port=%i" % port) + if (encoding is not None) and (encoding != ""): + cmd.append("--default-character-set=%s" % shlex_quote(encoding)) + if not all_databases: + cmd.append("--one-database") + cmd.append(shlex_quote(''.join(db_name))) + + comp_prog_path = None + if os.path.splitext(target)[-1] == '.gz': + comp_prog_path = module.get_bin_path('gzip', required=True) + elif os.path.splitext(target)[-1] == '.bz2': + comp_prog_path = module.get_bin_path('bzip2', required=True) + elif os.path.splitext(target)[-1] == '.xz': + comp_prog_path = module.get_bin_path('xz', required=True) + if comp_prog_path: + # The line below is for returned data only: + executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd)) + + if not use_shell: + p1 = subprocess.Popen([comp_prog_path, '-dc', target], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (stdout2, stderr2) = p2.communicate() + p1.stdout.close() + p1.wait() + + if p1.returncode != 0: + stderr1 = p1.stderr.read() + return p1.returncode, '', stderr1 + else: + return p2.returncode, stdout2, stderr2 + else: + # Used to prevent 'Broken pipe' errors that + # occasionaly occur when target files are compressed. + # FYI: passing the `shell=True` argument to p2 = subprocess.Popen() + # doesn't solve the problem. + cmd = " ".join(cmd) + cmd = "%s -dc %s | %s" % (comp_prog_path, shlex_quote(target), cmd) + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) + return rc, stdout, stderr + + else: + cmd = ' '.join(cmd) + cmd += " < %s" % shlex_quote(target) + executed_commands.append(cmd) + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) + return rc, stdout, stderr + + +def db_create(cursor, db, encoding, collation): + if not db: + return False + query_params = dict(enc=encoding, collate=collation) + res = 0 + for each_db in db: + # Escape '%' since mysql cursor.execute() uses a format string + query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database').replace('%', '%%')] + if encoding: + query.append("CHARACTER SET %(enc)s") + if collation: + query.append("COLLATE %(collate)s") + query = ' '.join(query) + res += cursor.execute(query, query_params) + try: + executed_commands.append(cursor.mogrify(query, query_params)) + except AttributeError: + executed_commands.append(cursor._executed) + except Exception: + executed_commands.append(query) + return res > 0 + + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + name=dict(type='list', required=True, aliases=['db']), + encoding=dict(type='str', default=''), + collation=dict(type='str', default=''), + target=dict(type='path'), + state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']), + single_transaction=dict(type='bool', default=False), + quick=dict(type='bool', default=True), + ignore_tables=dict(type='list', default=[]), + hex_blob=dict(default=False, type='bool'), + force=dict(type='bool', default=False), + master_data=dict(type='int', default=0, choices=[0, 1, 2]), + skip_lock_tables=dict(type='bool', default=False), + dump_extra_args=dict(type='str'), + use_shell=dict(type='bool', default=False), + unsafe_login_password=dict(type='bool', default=False, no_log=True), + restrict_config_file=dict(type='bool', default=False), + check_implicit_admin=dict(type='bool', default=False), + config_overrides_defaults=dict(type='bool', default=False), + chdir=dict(type='path'), + pipefail=dict(type='bool', default=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + + db = module.params["name"] + if not db: + module.exit_json(changed=False, db=db, db_list=[]) + db = [each_db.strip() for each_db in db] + + encoding = module.params["encoding"] + collation = module.params["collation"] + state = module.params["state"] + target = module.params["target"] + socket = module.params["login_unix_socket"] + login_port = module.params["login_port"] + if login_port < 0 or login_port > 65535: + module.fail_json(msg="login_port must be a valid unix port number (0-65535)") + ssl_cert = module.params["client_cert"] + ssl_key = module.params["client_key"] + ssl_ca = module.params["ca_cert"] + check_hostname = module.params["check_hostname"] + connect_timeout = module.params['connect_timeout'] + config_file = module.params['config_file'] + login_password = module.params["login_password"] + unsafe_login_password = module.params["unsafe_login_password"] + login_user = module.params["login_user"] + login_host = module.params["login_host"] + ignore_tables = module.params["ignore_tables"] + for a_table in ignore_tables: + if a_table == "": + module.fail_json(msg="Name of ignored table cannot be empty") + single_transaction = module.params["single_transaction"] + quick = module.params["quick"] + hex_blob = module.params["hex_blob"] + force = module.params["force"] + master_data = module.params["master_data"] + skip_lock_tables = module.params["skip_lock_tables"] + dump_extra_args = module.params["dump_extra_args"] + use_shell = module.params["use_shell"] + restrict_config_file = module.params["restrict_config_file"] + check_implicit_admin = module.params['check_implicit_admin'] + config_overrides_defaults = module.params['config_overrides_defaults'] + chdir = module.params['chdir'] + pipefail = module.params['pipefail'] + + if chdir: + try: + os.chdir(chdir) + except Exception as e: + module.fail_json("Cannot change the current directory to %s: %s" % (chdir, e)) + + if len(db) > 1 and state == 'import': + module.fail_json(msg="Multiple databases are not supported with state=import") + db_name = ' '.join(db) + + all_databases = False + if state in ['dump', 'import']: + if target is None: + module.fail_json(msg="with state=%s target is required" % state) + if db == ['all']: + all_databases = True + else: + if db == ['all']: + module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.") + try: + cursor = None + if check_implicit_admin: + try: + cursor, db_conn = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca, + connect_timeout=connect_timeout, check_hostname=check_hostname, + config_overrides_defaults=config_overrides_defaults) + except Exception as e: + check_implicit_admin = False + pass + + if not cursor: + cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, + connect_timeout=connect_timeout, config_overrides_defaults=config_overrides_defaults, + check_hostname=check_hostname) + except Exception as e: + if os.path.exists(config_file): + module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " + "Exception message: %s" % (config_file, to_native(e))) + else: + module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) + + changed = False + if not os.path.exists(config_file): + config_file = None + + existence_list = [] + non_existence_list = [] + + if not all_databases: + for each_database in db: + if db_exists(cursor, [each_database]): + existence_list.append(each_database) + else: + non_existence_list.append(each_database) + + if state == "absent": + if module.check_mode: + module.exit_json(changed=bool(existence_list), db=db_name, db_list=db) + try: + changed = db_delete(cursor, existence_list) + except Exception as e: + module.fail_json(msg="error deleting database: %s" % to_native(e)) + module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands) + elif state == "present": + if module.check_mode: + module.exit_json(changed=bool(non_existence_list), db=db_name, db_list=db) + changed = False + if non_existence_list: + try: + changed = db_create(cursor, non_existence_list, encoding, collation) + except Exception as e: + module.fail_json(msg="error creating database: %s" % to_native(e), + exception=traceback.format_exc()) + module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands) + elif state == "dump": + if non_existence_list and not all_databases: + module.fail_json(msg="Cannot dump database(s) %r - not found" % (', '.join(non_existence_list))) + if module.check_mode: + module.exit_json(changed=True, db=db_name, db_list=db) + rc, stdout, stderr = db_dump(module, login_host, login_user, + login_password, db, target, all_databases, + login_port, config_file, socket, ssl_cert, ssl_key, + ssl_ca, single_transaction, quick, ignore_tables, + hex_blob, encoding, force, master_data, skip_lock_tables, + dump_extra_args, unsafe_login_password, restrict_config_file, + check_implicit_admin, pipefail) + if rc != 0: + module.fail_json(msg="%s" % stderr) + module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout, + executed_commands=executed_commands) + elif state == "import": + if module.check_mode: + module.exit_json(changed=True, db=db_name, db_list=db) + if non_existence_list and not all_databases: + try: + db_create(cursor, non_existence_list, encoding, collation) + except Exception as e: + module.fail_json(msg="error creating database: %s" % to_native(e), + exception=traceback.format_exc()) + rc, stdout, stderr = db_import(module, login_host, login_user, + login_password, db, target, + all_databases, + login_port, config_file, + socket, ssl_cert, ssl_key, ssl_ca, + encoding, force, use_shell, unsafe_login_password, + restrict_config_file, check_implicit_admin) + if rc != 0: + module.fail_json(msg="%s" % stderr) + module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout, + executed_commands=executed_commands) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_info.py b/ansible_collections/community/mysql/plugins/modules/mysql_info.py new file mode 100644 index 000000000..11b1a8003 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_info.py @@ -0,0 +1,605 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_info +short_description: Gather information about MySQL servers +description: +- Gathers information about MySQL servers. + +options: + filter: + description: + - Limit the collected information by comma separated string or YAML list. + - Allowable values are C(version), C(databases), C(settings), C(global_status), + C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts). + - By default, collects all subsets. + - You can use '!' before value (for example, C(!settings)) to exclude it from the information. + - If you pass including and excluding values to the filter, for example, I(filter=!settings,version), + the excluding values, C(!settings) in this case, will be ignored. + type: list + elements: str + login_db: + description: + - Database name to connect to. + - It makes sense if I(login_user) is allowed to connect to a specific database only. + type: str + exclude_fields: + description: + - List of fields which are not needed to collect. + - "Supports elements: C(db_size). Unsupported elements will be ignored." + type: list + elements: str + version_added: '0.1.0' + return_empty_dbs: + description: + - Includes names of empty databases to returned dictionary. + type: bool + default: false + +notes: +- Calculating the size of a database might be slow, depending on the number and size of tables in it. + To avoid this, use I(exclude_fields=db_size). +- Supports C(check_mode). + +seealso: +- module: community.mysql.mysql_variables +- module: community.mysql.mysql_db +- module: community.mysql.mysql_user +- module: community.mysql.mysql_replication + +author: +- Andrew Klychkov (@Andersson007) +- Sebastian Gumprich (@rndmh3ro) +- Laurent Indermühle (@laurent-indermuehle) + +extends_documentation_fragment: +- community.mysql.mysql +''' + +EXAMPLES = r''' +# Display info from mysql-hosts group (using creds from ~/.my.cnf to connect): +# ansible mysql-hosts -m mysql_info + +# Display only databases and users info: +# ansible mysql-hosts -m mysql_info -a 'filter=databases,users' + +# Display only slave status: +# ansible standby -m mysql_info -a 'filter=slave_status' + +# Display all info from databases group except settings: +# ansible databases -m mysql_info -a 'filter=!settings' + +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Collect all possible information using passwordless root access + community.mysql.mysql_info: + login_user: root + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Get MySQL version with non-default credentials + community.mysql.mysql_info: + login_user: mysuperuser + login_password: mysuperpass + filter: version + +- name: Collect all info except settings and users by root + community.mysql.mysql_info: + login_user: root + login_password: rootpass + filter: "!settings,!users" + +- name: Collect info about databases and version using ~/.my.cnf as a credential file + become: true + community.mysql.mysql_info: + filter: + - databases + - version + +- name: Collect info about databases and version using ~alice/.my.cnf as a credential file + become: true + community.mysql.mysql_info: + config_file: /home/alice/.my.cnf + filter: + - databases + - version + +- name: Collect info about databases including empty and excluding their sizes + become: true + community.mysql.mysql_info: + config_file: /home/alice/.my.cnf + filter: + - databases + exclude_fields: db_size + return_empty_dbs: true +''' + +RETURN = r''' +version: + description: Database server version. + returned: if not excluded by filter + type: dict + sample: { "version": { "major": 5, "minor": 5, "release": 60, "suffix": "MariaDB", "full": "5.5.60-MariaDB" } } + contains: + major: + description: Major server version. + returned: if not excluded by filter + type: int + sample: 5 + minor: + description: Minor server version. + returned: if not excluded by filter + type: int + sample: 5 + release: + description: Release server version. + returned: if not excluded by filter + type: int + sample: 60 + suffix: + description: Server suffix, for example MySQL, MariaDB, other or none. + returned: if not excluded by filter + type: str + sample: "MariaDB" + full: + description: Full server version. + returned: if not excluded by filter + type: str + sample: "5.5.60-MariaDB" +databases: + description: Information about databases. + returned: if not excluded by filter + type: dict + sample: + - { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } } + contains: + size: + description: Database size in bytes. + returned: if not excluded by filter + type: dict + sample: { 'size': 656594 } +settings: + description: Global settings (variables) information. + returned: if not excluded by filter + type: dict + sample: + - { "innodb_open_files": 300, innodb_page_size": 16384 } +global_status: + description: Global status information. + returned: if not excluded by filter + type: dict + sample: + - { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 } +users: + description: Users information. + returned: if not excluded by filter + type: dict + sample: + - { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } } +engines: + description: Information about the server's storage engines. + returned: if not excluded by filter + type: dict + sample: + - { "CSV": { "Comment": "CSV storage engine", "Savepoints": "NO", "Support": "YES", "Transactions": "NO", "XA": "NO" } } +master_status: + description: Master status information. + returned: if master + type: dict + sample: + - { "Binlog_Do_DB": "", "Binlog_Ignore_DB": "mysql", "File": "mysql-bin.000001", "Position": 769 } +slave_status: + description: Slave status information. + returned: if standby + type: dict + sample: + - { "192.168.1.101": { "3306": { "replication_user": { "Connect_Retry": 60, "Exec_Master_Log_Pos": 769, "Last_Errno": 0 } } } } +slave_hosts: + description: Slave status information. + returned: if master + type: dict + sample: + - { "2": { "Host": "", "Master_id": 1, "Port": 3306 } } +connector_name: + description: Name of the python connector used by the module. When the connector is not identified, returns C(Unknown). + returned: always + type: str + sample: + - "pymysql" + - "MySQLdb" + version_added: '3.6.0' +connector_version: + description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown). + returned: always + type: str + sample: + - "1.0.2" + version_added: '3.6.0' +''' + +from decimal import Decimal + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_common_argument_spec, + mysql_driver, + mysql_driver_fail_msg, + get_connector_name, + get_connector_version, +) +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_native + + +# =========================================== +# MySQL module specific support methods. +# + +class MySQL_Info(object): + + """Class for collection MySQL instance information. + + Arguments: + module (AnsibleModule): Object of AnsibleModule class. + cursor (pymysql/mysql-python): Cursor class for interaction with + the database. + + Note: + If you need to add a new subset: + 1. add a new key with the same name to self.info attr in self.__init__() + 2. add a new private method to get the information + 3. add invocation of the new method to self.__collect() + 4. add info about the new subset to the DOCUMENTATION block + 5. add info about the new subset with an example to RETURN block + """ + + def __init__(self, module, cursor): + self.module = module + self.cursor = cursor + self.info = { + 'version': {}, + 'databases': {}, + 'settings': {}, + 'global_status': {}, + 'engines': {}, + 'users': {}, + 'master_status': {}, + 'slave_hosts': {}, + 'slave_status': {}, + } + + def get_info(self, filter_, exclude_fields, return_empty_dbs): + """Get MySQL instance information based on filter_. + + Arguments: + filter_ (list): List of collected subsets (e.g., databases, users, etc.), + when it is empty, return all available information. + """ + + inc_list = [] + exc_list = [] + + if filter_: + partial_info = {} + + for fi in filter_: + if fi.lstrip('!') not in self.info: + self.module.warn('filter element: %s is not allowable, ignored' % fi) + continue + + if fi[0] == '!': + exc_list.append(fi.lstrip('!')) + + else: + inc_list.append(fi) + + if inc_list: + self.__collect(exclude_fields, return_empty_dbs, set(inc_list)) + + for i in self.info: + if i in inc_list: + partial_info[i] = self.info[i] + + else: + not_in_exc_list = list(set(self.info) - set(exc_list)) + self.__collect(exclude_fields, return_empty_dbs, set(not_in_exc_list)) + + for i in self.info: + if i not in exc_list: + partial_info[i] = self.info[i] + + return partial_info + + else: + self.__collect(exclude_fields, return_empty_dbs, set(self.info)) + return self.info + + def __collect(self, exclude_fields, return_empty_dbs, wanted): + """Collect all possible subsets.""" + if 'version' in wanted or 'settings' in wanted: + self.__get_global_variables() + + if 'databases' in wanted: + self.__get_databases(exclude_fields, return_empty_dbs) + + if 'global_status' in wanted: + self.__get_global_status() + + if 'engines' in wanted: + self.__get_engines() + + if 'users' in wanted: + self.__get_users() + + if 'master_status' in wanted: + self.__get_master_status() + + if 'slave_status' in wanted: + self.__get_slave_status() + + if 'slave_hosts' in wanted: + self.__get_slaves() + + def __get_engines(self): + """Get storage engines info.""" + res = self.__exec_sql('SHOW ENGINES') + + if res: + for line in res: + engine = line['Engine'] + self.info['engines'][engine] = {} + + for vname, val in iteritems(line): + if vname != 'Engine': + self.info['engines'][engine][vname] = val + + def __convert(self, val): + """Convert unserializable data.""" + try: + if isinstance(val, Decimal): + val = float(val) + else: + val = int(val) + + except ValueError: + pass + + except TypeError: + pass + + return val + + def __get_global_variables(self): + """Get global variables (instance settings).""" + res = self.__exec_sql('SHOW GLOBAL VARIABLES') + + if res: + for var in res: + self.info['settings'][var['Variable_name']] = self.__convert(var['Value']) + + # version = ["5", "5," "60-MariaDB] + version = self.info['settings']['version'].split('.') + + # full_version = "5.5.60-MariaDB" + full = self.info['settings']['version'] + + # release = "60" + release = version[2].split('-')[0] + + # check if a suffix exists by counting the length + if len(version[2].split('-')) > 1: + # suffix = "MariaDB" + suffix = version[2].split('-', 1)[1] + else: + suffix = "" + + self.info['version'] = dict( + # major = "5" + major=int(version[0]), + # minor = "5" + minor=int(version[1]), + release=int(release), + suffix=str(suffix), + full=str(full), + ) + + def __get_global_status(self): + """Get global status.""" + res = self.__exec_sql('SHOW GLOBAL STATUS') + + if res: + for var in res: + self.info['global_status'][var['Variable_name']] = self.__convert(var['Value']) + + def __get_master_status(self): + """Get master status if the instance is a master.""" + res = self.__exec_sql('SHOW MASTER STATUS') + if res: + for line in res: + for vname, val in iteritems(line): + self.info['master_status'][vname] = self.__convert(val) + + def __get_slave_status(self): + """Get slave status if the instance is a slave.""" + res = self.__exec_sql('SHOW SLAVE STATUS') + if res: + for line in res: + host = line['Master_Host'] + if host not in self.info['slave_status']: + self.info['slave_status'][host] = {} + + port = line['Master_Port'] + if port not in self.info['slave_status'][host]: + self.info['slave_status'][host][port] = {} + + user = line['Master_User'] + if user not in self.info['slave_status'][host][port]: + self.info['slave_status'][host][port][user] = {} + + for vname, val in iteritems(line): + if vname not in ('Master_Host', 'Master_Port', 'Master_User'): + self.info['slave_status'][host][port][user][vname] = self.__convert(val) + + def __get_slaves(self): + """Get slave hosts info if the instance is a master.""" + res = self.__exec_sql('SHOW SLAVE HOSTS') + if res: + for line in res: + srv_id = line['Server_id'] + if srv_id not in self.info['slave_hosts']: + self.info['slave_hosts'][srv_id] = {} + + for vname, val in iteritems(line): + if vname != 'Server_id': + self.info['slave_hosts'][srv_id][vname] = self.__convert(val) + + def __get_users(self): + """Get user info.""" + res = self.__exec_sql('SELECT * FROM mysql.user') + if res: + for line in res: + host = line['Host'] + if host not in self.info['users']: + self.info['users'][host] = {} + + user = line['User'] + self.info['users'][host][user] = {} + + for vname, val in iteritems(line): + if vname not in ('Host', 'User'): + self.info['users'][host][user][vname] = self.__convert(val) + + def __get_databases(self, exclude_fields, return_empty_dbs): + """Get info about databases.""" + if not exclude_fields: + query = ('SELECT table_schema AS "name", ' + 'SUM(data_length + index_length) AS "size" ' + 'FROM information_schema.TABLES GROUP BY table_schema') + else: + if 'db_size' in exclude_fields: + query = ('SELECT table_schema AS "name" ' + 'FROM information_schema.TABLES GROUP BY table_schema') + + res = self.__exec_sql(query) + + if res: + for db in res: + self.info['databases'][db['name']] = {} + + if not exclude_fields or 'db_size' not in exclude_fields: + if db['size'] is None: + db['size'] = 0 + + self.info['databases'][db['name']]['size'] = int(db['size']) + + # If empty dbs are not needed in the returned dict, exit from the method + if not return_empty_dbs: + return None + + # Add info about empty databases (issue #65727): + res = self.__exec_sql('SHOW DATABASES') + if res: + for db in res: + if db['Database'] not in self.info['databases']: + self.info['databases'][db['Database']] = {} + + if not exclude_fields or 'db_size' not in exclude_fields: + self.info['databases'][db['Database']]['size'] = 0 + + def __exec_sql(self, query, ddl=False): + """Execute SQL. + + Arguments: + ddl (bool): If True, return True or False. + Used for queries that don't return any rows + (mainly for DDL queries) (default False). + """ + try: + self.cursor.execute(query) + + if not ddl: + res = self.cursor.fetchall() + return res + return True + + except Exception as e: + self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e))) + return False + + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + login_db=dict(type='str'), + filter=dict(type='list'), + exclude_fields=dict(type='list'), + return_empty_dbs=dict(type='bool', default=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + db = module.params['login_db'] + connect_timeout = module.params['connect_timeout'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + ssl_cert = module.params['client_cert'] + ssl_key = module.params['client_key'] + ssl_ca = module.params['ca_cert'] + check_hostname = module.params['check_hostname'] + config_file = module.params['config_file'] + filter_ = module.params['filter'] + exclude_fields = module.params['exclude_fields'] + return_empty_dbs = module.params['return_empty_dbs'] + + if filter_: + filter_ = [f.strip() for f in filter_] + + if exclude_fields: + exclude_fields = set([f.strip() for f in exclude_fields]) + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + + connector_name = get_connector_name(mysql_driver) + connector_version = get_connector_version(mysql_driver) + + try: + cursor, db_conn = mysql_connect(module, login_user, login_password, + config_file, ssl_cert, ssl_key, ssl_ca, db, + check_hostname=check_hostname, + connect_timeout=connect_timeout, cursor_class='DictCursor') + except Exception as e: + msg = ('unable to connect to database using %s %s, check login_user ' + 'and login_password are correct or %s has the credentials. ' + 'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e))) + module.fail_json(msg) + + ############################### + # Create object and do main job + + mysql = MySQL_Info(module, cursor) + + module.exit_json(changed=False, + connector_name=connector_name, + connector_version=connector_version, + **mysql.get_info(filter_, exclude_fields, return_empty_dbs)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_query.py b/ansible_collections/community/mysql/plugins/modules/mysql_query.py new file mode 100644 index 000000000..12d5a5630 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_query.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_query +short_description: Run MySQL queries +description: +- Runs arbitrary MySQL queries. +- Pay attention, the module does not support check mode! + All queries will be executed in autocommit mode. +- To run SQL queries from a file, use M(community.mysql.mysql_db) module. +version_added: '0.1.0' +options: + query: + description: + - SQL query to run. Multiple queries can be passed using YAML list syntax. + - Must be a string or YAML list containing strings. + - If you use I(named_args) or I(positional_args) any C(%) will be interpreted + as a formatting character. All literal C(%) characters in the query should be + escaped as C(%%). + - Note that if you use the C(IF EXISTS/IF NOT EXISTS) clauses in your query + and C(mysqlclient) or C(PyMySQL 0.10.0+) connectors, the module will report + that the state has been changed even if it has not. If it is important in your + workflow, use the C(PyMySQL 0.9.3) connector instead. + type: raw + required: true + positional_args: + description: + - List of values to be passed as positional arguments to the query. + - Mutually exclusive with I(named_args). + type: list + named_args: + description: + - Dictionary of key-value arguments to pass to the query. + - Mutually exclusive with I(positional_args). + type: dict + login_db: + description: + - Name of database to connect to and run queries against. + type: str + single_transaction: + description: + - Where passed queries run in a single transaction (C(yes)) or commit them one-by-one (C(no)). + type: bool + default: false +seealso: +- module: community.mysql.mysql_db +author: +- Andrew Klychkov (@Andersson007) +extends_documentation_fragment: +- community.mysql.mysql + +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Simple select query to acme db + community.mysql.mysql_query: + login_db: acme + query: SELECT * FROM orders + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Select query to db acme with positional arguments + community.mysql.mysql_query: + login_db: acme + query: SELECT * FROM acme WHERE id = %s AND story = %s + positional_args: + - 1 + - test + +- name: Select query to test_db with named_args + community.mysql.mysql_query: + login_db: test_db + query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s + named_args: + id_val: 1 + story_val: test + +- name: Run several insert queries against db test_db in single transaction + community.mysql.mysql_query: + login_db: test_db + query: + - INSERT INTO articles (id, story) VALUES (2, 'my_long_story') + - INSERT INTO prices (id, price) VALUES (123, '100.00') + single_transaction: true +''' + +RETURN = r''' +executed_queries: + description: List of executed queries. + returned: always + type: list + sample: ['SELECT * FROM bar', 'UPDATE bar SET id = 1 WHERE id = 2'] +query_result: + description: + - List of lists (sublist for each query) containing dictionaries + in column:value form representing returned rows. + returned: changed + type: list + sample: [[{"Column": "Value1"},{"Column": "Value2"}], [{"ID": 1}, {"ID": 2}]] +rowcount: + description: Number of affected rows for each subquery. + returned: changed + type: list + sample: [5, 1] +''' + +import warnings + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_common_argument_spec, + mysql_driver, + mysql_driver_fail_msg, +) +from ansible.module_utils._text import to_native + +DML_QUERY_KEYWORDS = ('INSERT', 'UPDATE', 'DELETE', 'REPLACE') +# TRUNCATE is not DDL query but it also returns 0 rows affected: +DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE') + + +# =========================================== +# Module execution. +# + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + query=dict(type='raw', required=True), + login_db=dict(type='str'), + positional_args=dict(type='list'), + named_args=dict(type='dict'), + single_transaction=dict(type='bool', default=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=( + ('positional_args', 'named_args'), + ), + ) + + db = module.params['login_db'] + connect_timeout = module.params['connect_timeout'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + ssl_cert = module.params['client_cert'] + ssl_key = module.params['client_key'] + ssl_ca = module.params['ca_cert'] + check_hostname = module.params['check_hostname'] + config_file = module.params['config_file'] + query = module.params["query"] + + if not isinstance(query, (str, list)): + module.fail_json(msg="the query option value must be a string or list, passed %s" % type(query)) + + if isinstance(query, str): + query = [query] + + for elem in query: + if not isinstance(elem, str): + module.fail_json(msg="the elements in query list must be strings, passed '%s' %s" % (elem, type(elem))) + + if module.params["single_transaction"]: + autocommit = False + else: + autocommit = True + # Prepare args: + if module.params.get("positional_args"): + arguments = module.params["positional_args"] + elif module.params.get("named_args"): + arguments = module.params["named_args"] + else: + arguments = None + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + + # Connect to DB: + try: + cursor, db_connection = mysql_connect(module, login_user, login_password, + config_file, ssl_cert, ssl_key, ssl_ca, db, + check_hostname=check_hostname, + connect_timeout=connect_timeout, + cursor_class='DictCursor', autocommit=autocommit) + except Exception as e: + module.fail_json(msg="unable to connect to database, check login_user and " + "login_password are correct or %s has the credentials. " + "Exception message: %s" % (config_file, to_native(e))) + + # Set defaults: + changed = False + + max_keyword_len = len(max(DML_QUERY_KEYWORDS + DDL_QUERY_KEYWORDS, key=len)) + + # Execute query: + query_result = [] + executed_queries = [] + rowcount = [] + + already_exists = False + for q in query: + try: + with warnings.catch_warnings(): + warnings.filterwarnings(action='error', + message='.*already exists*', + category=mysql_driver.Warning) + + try: + cursor.execute(q, arguments) + except mysql_driver.Warning: + # When something is run with IF NOT EXISTS + # and there's "already exists" MySQL warning, + # set the flag as True. + # PyMySQL < 0.10.0 throws the warning, mysqlclient + # and PyMySQL 0.10.0+ does NOT. + already_exists = True + + except Exception as e: + if not autocommit: + db_connection.rollback() + + cursor.close() + module.fail_json(msg="Cannot execute SQL '%s' args [%s]: %s" % (q, arguments, to_native(e))) + + try: + if not already_exists: + query_result.append([dict(row) for row in cursor.fetchall()]) + + except Exception as e: + if not autocommit: + db_connection.rollback() + + module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e)) + + # Check DML or DDL keywords in query and set changed accordingly: + q = q.lstrip()[0:max_keyword_len].upper() + for keyword in DML_QUERY_KEYWORDS: + if keyword in q and cursor.rowcount > 0: + changed = True + + for keyword in DDL_QUERY_KEYWORDS: + if keyword in q: + if already_exists: + # Indicates the entity already exists + changed = False + already_exists = False # Reset flag + else: + changed = True + try: + executed_queries.append(cursor._last_executed) + except AttributeError: + # MySQLdb removed cursor._last_executed as a duplicate of cursor._executed + executed_queries.append(cursor._executed) + rowcount.append(cursor.rowcount) + + # When the module run with the single_transaction == True: + if not autocommit: + db_connection.commit() + + # Create dict with returned values: + kw = { + 'changed': changed, + 'executed_queries': executed_queries, + 'query_result': query_result, + 'rowcount': rowcount, + } + + # Exit: + module.exit_json(**kw) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_replication.py b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py new file mode 100644 index 000000000..33e14bc26 --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py @@ -0,0 +1,654 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com> +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# Certain parts are taken from Mark Theunissen's mysqldb module +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: mysql_replication +short_description: Manage MySQL replication +description: +- Manages MySQL server replication, replica, primary status, get and change primary host. +author: +- Balazs Pocze (@banyek) +- Andrew Klychkov (@Andersson007) +options: + mode: + description: + - Module operating mode. Could be + C(changeprimary) (CHANGE PRIMARY TO), + C(getprimary) (SHOW PRIMARY STATUS), + C(getreplica) (SHOW REPLICA), + C(startreplica) (START REPLICA), + C(stopreplica) (STOP REPLICA), + C(resetprimary) (RESET PRIMARY) - supported since community.mysql 0.1.0, + C(resetreplica) (RESET REPLICA), + C(resetreplicaall) (RESET REPLICA ALL). + type: str + choices: + - changeprimary + - getprimary + - getreplica + - startreplica + - stopreplica + - resetprimary + - resetreplica + - resetreplicaall + default: getreplica + primary_host: + description: + - Same as the C(MASTER_HOST) mysql variable. + type: str + aliases: [master_host] + primary_user: + description: + - Same as the C(MASTER_USER) mysql variable. + type: str + aliases: [master_user] + primary_password: + description: + - Same as the C(MASTER_PASSWORD) mysql variable. + type: str + aliases: [master_password] + primary_port: + description: + - Same as the C(MASTER_PORT) mysql variable. + type: int + aliases: [master_port] + primary_connect_retry: + description: + - Same as the C(MASTER_CONNECT_RETRY) mysql variable. + type: int + aliases: [master_connect_retry] + primary_log_file: + description: + - Same as the C(MASTER_LOG_FILE) mysql variable. + type: str + aliases: [master_log_file] + primary_log_pos: + description: + - Same as the C(MASTER_LOG_POS) mysql variable. + type: int + aliases: [master_log_pos] + relay_log_file: + description: + - Same as mysql variable. + type: str + relay_log_pos: + description: + - Same as mysql variable. + type: int + primary_ssl: + description: + - Same as the C(MASTER_SSL) mysql variable. + - When setting it to C(yes), the connection attempt only succeeds + if an encrypted connection can be established. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + - The default is C(false). + type: bool + aliases: [master_ssl] + primary_ssl_ca: + description: + - Same as the C(MASTER_SSL_CA) mysql variable. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + type: str + aliases: [master_ssl_ca] + primary_ssl_capath: + description: + - Same as the C(MASTER_SSL_CAPATH) mysql variable. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + type: str + aliases: [master_ssl_capath] + primary_ssl_cert: + description: + - Same as the C(MASTER_SSL_CERT) mysql variable. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + type: str + aliases: [master_ssl_cert] + primary_ssl_key: + description: + - Same as the C(MASTER_SSL_KEY) mysql variable. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + type: str + aliases: [master_ssl_key] + primary_ssl_cipher: + description: + - Same as the C(MASTER_SSL_CIPHER) mysql variable. + - Specifies a colon-separated list of one or more ciphers permitted by the replica for the replication connection. + - For details, refer to + L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + type: str + aliases: [master_ssl_cipher] + primary_ssl_verify_server_cert: + description: + - Same as mysql variable. + type: bool + default: false + version_added: '3.5.0' + primary_auto_position: + description: + - Whether the host uses GTID based replication or not. + - Same as the C(MASTER_AUTO_POSITION) mysql variable. + type: bool + default: false + aliases: [master_auto_position] + primary_use_gtid: + description: + - Configures the replica to use the MariaDB Global Transaction ID. + - C(disabled) equals MASTER_USE_GTID=no command. + - To find information about available values see + U(https://mariadb.com/kb/en/library/change-master-to/#master_use_gtid). + - Available since MariaDB 10.0.2. + choices: [current_pos, replica_pos, disabled] + type: str + version_added: '0.1.0' + aliases: [master_use_gtid] + primary_delay: + description: + - Time lag behind the primary's state (in seconds). + - Same as the C(MASTER_DELAY) mysql variable. + - Available from MySQL 5.6. + - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-delayed.html). + type: int + version_added: '0.1.0' + aliases: [master_delay] + connection_name: + description: + - Name of the primary connection. + - Supported from MariaDB 10.0.1. + - Mutually exclusive with I(channel). + - For more information see U(https://mariadb.com/kb/en/library/multi-source-replication/). + type: str + version_added: '0.1.0' + channel: + description: + - Name of replication channel. + - Multi-source replication is supported from MySQL 5.7. + - Mutually exclusive with I(connection_name). + - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-multi-source.html). + type: str + version_added: '0.1.0' + fail_on_error: + description: + - Fails on error when calling mysql. + type: bool + default: false + version_added: '0.1.0' + +notes: +- If an empty value for the parameter of string type is needed, use an empty string. + +extends_documentation_fragment: +- community.mysql.mysql + + +seealso: +- module: community.mysql.mysql_info +- name: MySQL replication reference + description: Complete reference of the MySQL replication documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/replication.html +- name: MySQL encrypted replication reference. + description: Setting up MySQL replication to use encrypted connection. + link: https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html +- name: MariaDB replication reference + description: Complete reference of the MariaDB replication documentation. + link: https://mariadb.com/kb/en/library/setting-up-replication/ +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Stop mysql replica thread + community.mysql.mysql_replication: + mode: stopreplica + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Get primary binlog file name and binlog position + community.mysql.mysql_replication: + mode: getprimary + +- name: Change primary to primary server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 + community.mysql.mysql_replication: + mode: changeprimary + primary_host: 192.0.2.1 + primary_log_file: mysql-bin.000009 + primary_log_pos: 4578 + +- name: Check replica status using port 3308 + community.mysql.mysql_replication: + mode: getreplica + login_host: ansible.example.com + login_port: 3308 + +- name: On MariaDB change primary to use GTID current_pos + community.mysql.mysql_replication: + mode: changeprimary + primary_use_gtid: current_pos + +- name: Change primary to use replication delay 3600 seconds + community.mysql.mysql_replication: + mode: changeprimary + primary_host: 192.0.2.1 + primary_delay: 3600 + +- name: Start MariaDB replica with connection name primary-1 + community.mysql.mysql_replication: + mode: startreplica + connection_name: primary-1 + +- name: Stop replication in channel primary-1 + community.mysql.mysql_replication: + mode: stopreplica + channel: primary-1 + +- name: > + Run RESET MASTER command which will delete all existing binary log files + and reset the binary log index file on the primary + community.mysql.mysql_replication: + mode: resetprimary + +- name: Run start replica and fail the task on errors + community.mysql.mysql_replication: + mode: startreplica + connection_name: primary-1 + fail_on_error: true + +- name: Change primary and fail on error (like when replica thread is running) + community.mysql.mysql_replication: + mode: changeprimary + fail_on_error: true + +''' + +RETURN = r''' +queries: + description: List of executed queries which modified DB's state. + returned: always + type: list + sample: ["CHANGE MASTER TO MASTER_HOST='primary2.example.com',MASTER_PORT=3306"] + version_added: '0.1.0' +''' + +import os +import warnings + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + mysql_driver_fail_msg, + mysql_common_argument_spec, +) +from ansible.module_utils._text import to_native + +executed_queries = [] + + +def get_primary_status(cursor): + # TODO: when it's available to change on MySQL's side, + # change MASTER to PRIMARY using the approach from + # get_replica_status() function. Same for other functions. + cursor.execute("SHOW MASTER STATUS") + primarystatus = cursor.fetchone() + return primarystatus + + +def get_replica_status(cursor, connection_name='', channel='', term='REPLICA'): + if connection_name: + query = "SHOW %s '%s' STATUS" % (term, connection_name) + else: + query = "SHOW %s STATUS" % term + + if channel: + query += " FOR CHANNEL '%s'" % channel + + cursor.execute(query) + replica_status = cursor.fetchone() + return replica_status + + +def stop_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'): + if connection_name: + query = "STOP %s '%s'" % (term, connection_name) + else: + query = 'STOP %s' % term + + if channel: + query += " FOR CHANNEL '%s'" % channel + + try: + executed_queries.append(query) + cursor.execute(query) + stopped = True + except mysql_driver.Warning as e: + stopped = False + except Exception as e: + if fail_on_error: + module.fail_json(msg="STOP REPLICA failed: %s" % to_native(e)) + stopped = False + return stopped + + +def reset_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'): + if connection_name: + query = "RESET %s '%s'" % (term, connection_name) + else: + query = 'RESET %s' % term + + if channel: + query += " FOR CHANNEL '%s'" % channel + + try: + executed_queries.append(query) + cursor.execute(query) + reset = True + except mysql_driver.Warning as e: + reset = False + except Exception as e: + if fail_on_error: + module.fail_json(msg="RESET REPLICA failed: %s" % to_native(e)) + reset = False + return reset + + +def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'): + if connection_name: + query = "RESET %s '%s' ALL" % (term, connection_name) + else: + query = 'RESET %s ALL' % term + + if channel: + query += " FOR CHANNEL '%s'" % channel + + try: + executed_queries.append(query) + cursor.execute(query) + reset = True + except mysql_driver.Warning as e: + reset = False + except Exception as e: + if fail_on_error: + module.fail_json(msg="RESET REPLICA ALL failed: %s" % to_native(e)) + reset = False + return reset + + +def reset_primary(module, cursor, fail_on_error=False): + query = 'RESET MASTER' + try: + executed_queries.append(query) + cursor.execute(query) + reset = True + except mysql_driver.Warning as e: + reset = False + except Exception as e: + if fail_on_error: + module.fail_json(msg="RESET MASTER failed: %s" % to_native(e)) + reset = False + return reset + + +def start_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'): + if connection_name: + query = "START %s '%s'" % (term, connection_name) + else: + query = 'START %s' % term + + if channel: + query += " FOR CHANNEL '%s'" % channel + + try: + executed_queries.append(query) + cursor.execute(query) + started = True + except mysql_driver.Warning as e: + started = False + except Exception as e: + if fail_on_error: + module.fail_json(msg="START REPLICA failed: %s" % to_native(e)) + started = False + return started + + +def changeprimary(cursor, chm, connection_name='', channel=''): + if connection_name: + query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm)) + else: + query = 'CHANGE MASTER TO %s' % ','.join(chm) + + if channel: + query += " FOR CHANNEL '%s'" % channel + + executed_queries.append(query) + cursor.execute(query) + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + mode=dict(type='str', default='getreplica', choices=[ + 'getprimary', + 'getreplica', + 'changeprimary', + 'stopreplica', + 'startreplica', + 'resetprimary', + 'resetreplica', + 'resetreplicaall']), + primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']), + primary_host=dict(type='str', aliases=['master_host']), + primary_user=dict(type='str', aliases=['master_user']), + primary_password=dict(type='str', no_log=True, aliases=['master_password']), + primary_port=dict(type='int', aliases=['master_port']), + primary_connect_retry=dict(type='int', aliases=['master_connect_retry']), + primary_log_file=dict(type='str', aliases=['master_log_file']), + primary_log_pos=dict(type='int', aliases=['master_log_pos']), + relay_log_file=dict(type='str'), + relay_log_pos=dict(type='int'), + primary_ssl=dict(type='bool', aliases=['master_ssl']), + primary_ssl_ca=dict(type='str', aliases=['master_ssl_ca']), + primary_ssl_capath=dict(type='str', aliases=['master_ssl_capath']), + primary_ssl_cert=dict(type='str', aliases=['master_ssl_cert']), + primary_ssl_key=dict(type='str', no_log=False, aliases=['master_ssl_key']), + primary_ssl_cipher=dict(type='str', aliases=['master_ssl_cipher']), + primary_ssl_verify_server_cert=dict(type='bool', default=False), + primary_use_gtid=dict(type='str', choices=[ + 'current_pos', 'replica_pos', 'disabled'], aliases=['master_use_gtid']), + primary_delay=dict(type='int', aliases=['master_delay']), + connection_name=dict(type='str'), + channel=dict(type='str'), + fail_on_error=dict(type='bool', default=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['connection_name', 'channel'] + ], + ) + mode = module.params["mode"] + primary_host = module.params["primary_host"] + primary_user = module.params["primary_user"] + primary_password = module.params["primary_password"] + primary_port = module.params["primary_port"] + primary_connect_retry = module.params["primary_connect_retry"] + primary_log_file = module.params["primary_log_file"] + primary_log_pos = module.params["primary_log_pos"] + relay_log_file = module.params["relay_log_file"] + relay_log_pos = module.params["relay_log_pos"] + primary_ssl = module.params["primary_ssl"] + primary_ssl_ca = module.params["primary_ssl_ca"] + primary_ssl_capath = module.params["primary_ssl_capath"] + primary_ssl_cert = module.params["primary_ssl_cert"] + primary_ssl_key = module.params["primary_ssl_key"] + primary_ssl_cipher = module.params["primary_ssl_cipher"] + primary_ssl_verify_server_cert = module.params["primary_ssl_verify_server_cert"] + primary_auto_position = module.params["primary_auto_position"] + ssl_cert = module.params["client_cert"] + ssl_key = module.params["client_key"] + ssl_ca = module.params["ca_cert"] + check_hostname = module.params["check_hostname"] + connect_timeout = module.params['connect_timeout'] + config_file = module.params['config_file'] + primary_delay = module.params['primary_delay'] + if module.params.get("primary_use_gtid") == 'disabled': + primary_use_gtid = 'no' + else: + primary_use_gtid = module.params["primary_use_gtid"] + connection_name = module.params["connection_name"] + channel = module.params['channel'] + fail_on_error = module.params['fail_on_error'] + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + else: + warnings.filterwarnings('error', category=mysql_driver.Warning) + + login_password = module.params["login_password"] + login_user = module.params["login_user"] + + try: + cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, + ssl_cert, ssl_key, ssl_ca, None, cursor_class='DictCursor', + connect_timeout=connect_timeout, check_hostname=check_hostname) + except Exception as e: + if os.path.exists(config_file): + module.fail_json(msg="unable to connect to database, check login_user and " + "login_password are correct or %s has the credentials. " + "Exception message: %s" % (config_file, to_native(e))) + else: + module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) + + cursor.execute("SELECT VERSION()") + if 'mariadb' in cursor.fetchone()["VERSION()"].lower(): + from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl + else: + from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl + + # Since MySQL 8.0.22 and MariaDB 10.5.1, + # "REPLICA" must be used instead of "SLAVE" + if impl.uses_replica_terminology(cursor): + replica_term = 'REPLICA' + else: + replica_term = 'SLAVE' + if primary_use_gtid == 'replica_pos': + primary_use_gtid = 'slave_pos' + + if mode == 'getprimary': + status = get_primary_status(cursor) + if not isinstance(status, dict): + status = dict(Is_Primary=False, + msg="Server is not configured as mysql primary") + else: + status['Is_Primary'] = True + + module.exit_json(queries=executed_queries, **status) + + elif mode == "getreplica": + status = get_replica_status(cursor, connection_name, channel, replica_term) + if not isinstance(status, dict): + status = dict(Is_Replica=False, msg="Server is not configured as mysql replica") + else: + status['Is_Replica'] = True + + module.exit_json(queries=executed_queries, **status) + + elif mode == 'changeprimary': + chm = [] + result = {} + if primary_host is not None: + chm.append("MASTER_HOST='%s'" % primary_host) + if primary_user is not None: + chm.append("MASTER_USER='%s'" % primary_user) + if primary_password is not None: + chm.append("MASTER_PASSWORD='%s'" % primary_password) + if primary_port is not None: + chm.append("MASTER_PORT=%s" % primary_port) + if primary_connect_retry is not None: + chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry) + if primary_log_file is not None: + chm.append("MASTER_LOG_FILE='%s'" % primary_log_file) + if primary_log_pos is not None: + chm.append("MASTER_LOG_POS=%s" % primary_log_pos) + if primary_delay is not None: + chm.append("MASTER_DELAY=%s" % primary_delay) + if relay_log_file is not None: + chm.append("RELAY_LOG_FILE='%s'" % relay_log_file) + if relay_log_pos is not None: + chm.append("RELAY_LOG_POS=%s" % relay_log_pos) + if primary_ssl is not None: + if primary_ssl: + chm.append("MASTER_SSL=1") + else: + chm.append("MASTER_SSL=0") + if primary_ssl_ca is not None: + chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca) + if primary_ssl_capath is not None: + chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath) + if primary_ssl_cert is not None: + chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert) + if primary_ssl_key is not None: + chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key) + if primary_ssl_cipher is not None: + chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher) + if primary_ssl_verify_server_cert: + chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1") + if primary_auto_position: + chm.append("MASTER_AUTO_POSITION=1") + if primary_use_gtid is not None: + chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) + try: + changeprimary(cursor, chm, connection_name, channel) + except mysql_driver.Warning as e: + result['warning'] = to_native(e) + except Exception as e: + module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm)) + result['changed'] = True + module.exit_json(queries=executed_queries, **result) + elif mode == "startreplica": + started = start_replica(module, cursor, connection_name, channel, fail_on_error, replica_term) + if started is True: + module.exit_json(msg="Replica started ", changed=True, queries=executed_queries) + else: + module.exit_json(msg="Replica already started (Or cannot be started)", changed=False, queries=executed_queries) + elif mode == "stopreplica": + stopped = stop_replica(module, cursor, connection_name, channel, fail_on_error, replica_term) + if stopped is True: + module.exit_json(msg="Replica stopped", changed=True, queries=executed_queries) + else: + module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries) + elif mode == 'resetprimary': + reset = reset_primary(module, cursor, fail_on_error) + if reset is True: + module.exit_json(msg="Primary reset", changed=True, queries=executed_queries) + else: + module.exit_json(msg="Primary already reset", changed=False, queries=executed_queries) + elif mode == "resetreplica": + reset = reset_replica(module, cursor, connection_name, channel, fail_on_error, replica_term) + if reset is True: + module.exit_json(msg="Replica reset", changed=True, queries=executed_queries) + else: + module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries) + elif mode == "resetreplicaall": + reset = reset_replica_all(module, cursor, connection_name, channel, fail_on_error, replica_term) + if reset is True: + module.exit_json(msg="Replica reset", changed=True, queries=executed_queries) + else: + module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries) + + warnings.simplefilter("ignore") + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_role.py b/ansible_collections/community/mysql/plugins/modules/mysql_role.py new file mode 100644 index 000000000..070d7939d --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_role.py @@ -0,0 +1,1095 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Andrew Klychkov <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_role + +short_description: Adds, removes, or updates a MySQL role + +description: + - Adds, removes, or updates a MySQL role. + - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5. + +version_added: '2.2.0' + +options: + name: + description: + - Name of the role to add or remove. + type: str + required: true + + admin: + description: + - Supported by B(MariaDB). + - Name of the admin user of the role (the I(login_user), by default). + type: str + + priv: + description: + - "MySQL privileges string in the format: C(db.table:priv1,priv2)." + - "You can specify multiple privileges by separating each one using + a forward slash: C(db.table:priv/db.table:priv)." + - The format is based on MySQL C(GRANT) statement. + - Database and table names can be quoted, MySQL-style. + - If column privileges are used, the C(priv1,priv2) part must be + exactly as returned by a C(SHOW GRANT) statement. If not followed, + the module will always report changes. It includes grouping columns + by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))). + - Can be passed as a dictionary (see the examples). + - Supports GRANTs for procedures and functions + (see the examples for the M(community.mysql.mysql_user) module). + type: raw + + append_privs: + description: + - Append the privileges defined by the I(priv) option to the existing ones + for this role instead of overwriting them. Mutually exclusive with I(subtract_privs). + type: bool + default: false + + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + If set, invalid privileges in I(priv) are ignored. + Mutually exclusive with I(append_privs). + version_added: '3.2.0' + type: bool + default: false + + members: + description: + - List of members of the role. + - For users, use the format C(username@hostname). + Always specify the hostname part explicitly. + - For roles, use the format C(rolename). + - Mutually exclusive with I(admin). + type: list + elements: str + + append_members: + description: + - Add members defined by the I(members) option to the existing ones + for this role instead of overwriting them. + - Mutually exclusive with the I(detach_members) and I(admin) option. + type: bool + default: false + + detach_members: + description: + - Detaches members defined by the I(members) option from the role + instead of overwriting all the current members. + - Mutually exclusive with the I(append_members) and I(admin) option. + type: bool + default: false + + set_default_role_all: + description: + - Is not supported by MariaDB and is silently ignored when working with MariaDB. + - If C(yes), runs B(SET DEFAULT ROLE ALL TO) each of the I(members) when changed. + - If you want to avoid this behavior, set this option to C(no) explicitly. + type: bool + default: true + + state: + description: + - If C(present) and the role does not exist, creates the role. + - If C(present) and the role exists, does nothing or updates its attributes. + - If C(absent), removes the role. + type: str + choices: [ absent, present ] + default: present + + check_implicit_admin: + description: + - Check if mysql allows login as root/nopassword before trying supplied credentials. + - If success, passed I(login_user)/I(login_password) will be ignored. + type: bool + default: false + + members_must_exist: + description: + - When C(yes), the module fails if any user in I(members) does not exist. + - When C(no), users in I(members) which don't exist are simply skipped. + type: bool + default: true + +notes: + - Pay attention that the module runs C(SET DEFAULT ROLE ALL TO) + all the I(members) passed by default when the state has changed. + If you want to avoid this behavior, set I(set_default_role_all) to C(no). + - Supports C(check_mode). + +seealso: + - module: community.mysql.mysql_user + - name: MySQL role reference + description: Complete reference of the MySQL role documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/create-role.html + +author: + - Andrew Klychkov (@Andersson007) + - Felix Hamme (@betanummeric) + +extends_documentation_fragment: + - community.mysql.mysql +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument, for example, login_unix_socket: /run/mysqld/mysqld.sock + +# Example of a .my.cnf file content for setting a root password +# [client] +# user=root +# password=n<_665{vS43y +# +# Example of a privileges dictionary passed through the priv option +# priv: +# 'mydb.*': 'INSERT,UPDATE' +# 'anotherdb.*': 'SELECT' +# 'yetanotherdb.*': 'ALL' +# +# You can also use the string format like in the community.mysql.mysql_user module, for example +# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL +# +# For more examples on how to specify privileges, refer to the community.mysql.mysql_user module + +# Create a role developers with all database privileges +# and add alice and bob as members. +# The statement 'SET DEFAULT ROLE ALL' to them will be run. +- name: Create role developers, add members + community.mysql.mysql_role: + name: developers + state: present + priv: '*.*:ALL' + members: + - 'alice@%' + - 'bob@%' + +- name: Same as above but do not run SET DEFAULT ROLE ALL TO each member + community.mysql.mysql_role: + name: developers + state: present + priv: '*.*:ALL' + members: + - 'alice@%' + - 'bob@%' + set_default_role_all: false + +# Assuming that the role developers exists, +# add john to the current members +- name: Add members to an existing role + community.mysql.mysql_role: + name: developers + state: present + append_members: true + members: + - 'joe@localhost' + +# Create role readers with the SELECT privilege +# on all tables in the fiction database +- name: Create role developers, add members + community.mysql.mysql_role: + name: readers + state: present + priv: 'fiction.*:SELECT' + +# Assuming that the role readers exists, +# add the UPDATE privilege to the role on all tables in the fiction database +- name: Create role developers, add members + community.mysql.mysql_role: + name: readers + state: present + priv: 'fiction.*:UPDATE' + append_privs: true + +- name: Create role with the 'SELECT' and 'UPDATE' privileges in db1 and db2 + community.mysql.mysql_role: + state: present + name: foo + priv: + 'db1.*': 'SELECT,UPDATE' + 'db2.*': 'SELECT,UPDATE' + +- name: Remove joe from readers + community.mysql.mysql_role: + state: present + name: readers + members: + - 'joe@localhost' + detach_members: true + +- name: Remove the role readers if exists + community.mysql.mysql_role: + state: absent + name: readers + +- name: Example of using login_unix_socket to connect to the server + community.mysql.mysql_role: + name: readers + state: present + login_unix_socket: /var/run/mysqld/mysqld.sock + +# Pay attention that the admin cannot be changed later +# and will be ignored if a role currently exists. +# To change members, you need to run a separate task using the admin +# of the role as the login_user. +- name: On MariaDB, create the role readers with alice as its admin + community.mysql.mysql_role: + state: present + name: readers + admin: 'alice@%' + +- name: Create the role business, add the role marketing to members + community.mysql.mysql_role: + state: present + name: business + members: + - marketing + +- name: Ensure the role foo does not have the DELETE privilege + community.mysql.mysql_role: + state: present + name: foo + subtract_privs: true + priv: + 'db1.*': DELETE + +- name: Add some members to a role and skip not-existent users + community.mysql.mysql_role: + state: present + name: foo + append_members: true + members_must_exist: false + members: + - 'existing_user@localhost' + - 'not_existing_user@localhost' + +- name: Detach some members from a role and ignore not-existent users + community.mysql.mysql_role: + state: present + name: foo + detach_members: true + members_must_exist: false + members: + - 'existing_user@localhost' + - 'not_existing_user@localhost' +''' + +RETURN = '''#''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + mysql_driver_fail_msg, + mysql_common_argument_spec +) +from ansible_collections.community.mysql.plugins.module_utils.user import ( + convert_priv_dict_to_str, + get_impl, + get_mode, + user_mod, + privileges_grant, + privileges_unpack, +) +from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems + + +def normalize_users(module, users, is_mariadb=False): + """Normalize passed user names. + + Example of transformation: + ['user0'] => [('user0', '')] / ['user0'] => [('user0', '%')] + ['user0@host0'] => [('user0', 'host0')] + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + users (list): List of user names. + is_mariadb (bool): Flag indicating we are working with MariaDB + + Returns: + list: List of tuples like [('user0', ''), ('user0', 'host0')]. + """ + normalized_users = [] + + for user in users: + try: + tmp = user.split('@') + + if tmp[0] == '': + module.fail_json(msg="Member's name cannot be empty.") + + if len(tmp) == 1: + if not is_mariadb: + normalized_users.append((tmp[0], '%')) + else: + normalized_users.append((tmp[0], '')) + + elif len(tmp) == 2: + normalized_users.append((tmp[0], tmp[1])) + + except Exception as e: + msg = ('Error occured while parsing the name "%s": %s. ' + 'It must be in the format "username" or ' + '"username@hostname" ' % (user, to_native(e))) + module.fail_json(msg=msg) + + return normalized_users + + +class DbServer(): + """Class to fetch information from a database. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + role_impl (library): Corresponding library depending + on a server type (MariaDB or MySQL) + mariadb (bool): True if MariaDB, False otherwise. + roles_supported (bool): True if roles are supported, False otherwise. + users (set): Set of users existing in a DB in the form (username, hostname). + """ + def __init__(self, module, cursor): + self.module = module + self.cursor = cursor + self.role_impl = self.get_implementation() + self.mariadb = self.role_impl.is_mariadb() + self.roles_supported = self.role_impl.supports_roles(self.cursor) + self.users = set(self.__get_users()) + + def is_mariadb(self): + """Get info whether a DB server is a MariaDB instance. + + Returns: + self.mariadb: Attribute value. + """ + return self.mariadb + + def supports_roles(self): + """Get info whether a DB server supports roles. + + Returns: + self.roles_supported: Attribute value. + """ + return self.roles_supported + + def get_implementation(self): + """Get a current server implementation depending on its type. + + Returns: + library: Depending on a server type (MySQL or MariaDB). + """ + self.cursor.execute("SELECT VERSION()") + + if 'mariadb' in self.cursor.fetchone()[0].lower(): + import ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb.role as role_impl + else: + import ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.role as role_impl + + return role_impl + + def check_users_in_db(self, users): + """Check if users exist in a database. + + Args: + users (list): List of tuples (username, hostname) to check. + """ + for user in users: + if user not in self.users: + msg = 'User / role `%s` with host `%s` does not exist' % (user[0], user[1]) + self.module.fail_json(msg=msg) + + def filter_existing_users(self, users): + for user in users: + if user in self.users: + yield user + + def __get_users(self): + """Get users. + + Returns: + list: List of tuples (username, hostname). + """ + self.cursor.execute('SELECT User, Host FROM mysql.user') + return self.cursor.fetchall() + + def get_users(self): + """Get set of tuples (username, hostname) existing in a DB. + + Returns: + self.users: Attribute value. + """ + return self.users + + def get_grants(self, user, host): + """Get grants. + + Args: + user (str): User name + host (str): Host name + + Returns: + list: List of tuples like [(grant1,), (grant2,), ... ]. + """ + if host: + self.cursor.execute('SHOW GRANTS FOR %s@%s', (user, host)) + else: + self.cursor.execute('SHOW GRANTS FOR %s', (user,)) + + return self.cursor.fetchall() + + +class MySQLQueryBuilder(): + """Class to build and return queries specific to MySQL. + + Args: + name (str): Role name. + host (str): Role host. + + Attributes: + name (str): Role name. + host (str): Role host. + """ + def __init__(self, name, host): + self.name = name + self.host = host + + def role_exists(self): + """Return a query to check if a role with self.name and self.host exists in a database. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return 'SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', (self.name, self.host) + + def role_grant(self, user): + """Return a query to grant a role to a user or a role. + + Args: + user (tuple): User / role to grant the role to in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'GRANT %s@%s TO %s@%s', (self.name, self.host, user[0], user[1]) + else: + return 'GRANT %s@%s TO %s', (self.name, self.host, user[0]) + + def role_revoke(self, user): + """Return a query to revoke a role from a user or role. + + Args: + user (tuple): User / role to revoke the role from in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'REVOKE %s@%s FROM %s@%s', (self.name, self.host, user[0], user[1]) + else: + return 'REVOKE %s@%s FROM %s', (self.name, self.host, user[0]) + + def role_create(self, admin=None): + """Return a query to create a role. + + Args: + admin (tuple): Admin user in the form (username, hostname). + Because it is not supported by MySQL, we ignore it. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return 'CREATE ROLE %s', (self.name,) + + +class MariaDBQueryBuilder(): + """Class to build and return queries specific to MariaDB. + + Args: + name (str): Role name. + + Attributes: + name (str): Role name. + """ + def __init__(self, name): + self.name = name + + def role_exists(self): + """Return a query to check if a role with self.name exists in a database. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return "SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", (self.name,) + + def role_grant(self, user): + """Return a query to grant a role to a user or role. + + Args: + user (tuple): User / role to grant the role to in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'GRANT %s TO %s@%s', (self.name, user[0], user[1]) + else: + return 'GRANT %s TO %s', (self.name, user[0]) + + def role_revoke(self, user): + """Return a query to revoke a role from a user or role. + + Args: + user (tuple): User / role to revoke the role from in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'REVOKE %s FROM %s@%s', (self.name, user[0], user[1]) + else: + return 'REVOKE %s FROM %s', (self.name, user[0]) + + def role_create(self, admin=None): + """Return a query to create a role. + + Args: + admin (tuple): Admin user in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if not admin: + return 'CREATE ROLE %s', (self.name,) + + if admin[1]: + return 'CREATE ROLE %s WITH ADMIN %s@%s', (self.name, admin[0], admin[1]) + else: + return 'CREATE ROLE %s WITH ADMIN %s', (self.name, admin[0]) + + +class MySQLRoleImpl(): + """Class to work with MySQL role implementation. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + host (str): Role host. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + host (str): Role host. + """ + def __init__(self, module, cursor, name, host): + self.module = module + self.cursor = cursor + self.name = name + self.host = host + + def set_default_role_all(self, user): + """Run 'SET DEFAULT ROLE ALL TO' a user. + + Args: + user (tuple): User / role to run the command against in the form (username, hostname). + """ + if user[1]: + self.cursor.execute('SET DEFAULT ROLE ALL TO %s@%s', (user[0], user[1])) + else: + self.cursor.execute('SET DEFAULT ROLE ALL TO %s', (user[0],)) + + def get_admin(self): + """Get a current admin of a role. + + Not supported by MySQL, so ignored here. + """ + pass + + def set_admin(self, admin): + """Set an admin of a role. + + Not supported by MySQL, so ignored here. + + TODO: Implement the feature if this gets supported. + + Args: + admin (tuple): Admin user of the role in the form (username, hostname). + """ + pass + + +class MariaDBRoleImpl(): + """Class to work with MariaDB role implementation. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + """ + def __init__(self, module, cursor, name): + self.module = module + self.cursor = cursor + self.name = name + + def set_default_role_all(self, user): + """Run 'SET DEFAULT ROLE ALL TO' a user. + + The command is not supported by MariaDB, ignored. + + Args: + user (tuple): User / role to run the command against in the form (username, hostname). + """ + pass + + def get_admin(self): + """Get a current admin of a role. + + Returns: + tuple: Of the form (username, hostname). + """ + query = ("SELECT User, Host FROM mysql.roles_mapping " + "WHERE Role = %s and Admin_option = 'Y'") + + self.cursor.execute(query, (self.name,)) + return self.cursor.fetchone() + + def set_admin(self, admin): + """Set an admin of a role. + + TODO: Implement changing when ALTER ROLE statement to + change role's admin gets supported. + + Args: + admin (tuple): Admin user of the role in the form (username, hostname). + """ + admin_user = admin[0] + admin_host = admin[1] + current_admin = self.get_admin() + + if (admin_user, admin_host) != current_admin: + msg = ('The "admin" option value and the current ' + 'roles admin (%s@%s) don not match. Ignored. ' + 'To change the admin, you need to drop and create the ' + 'role again.' % (current_admin[0], current_admin[1])) + self.module.warn(msg) + + +class Role(): + """Class to work with MySQL role objects. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + server (DbServer): Object of the DbServer class. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + server (DbServer): Object of the DbServer class. + host (str): Role's host. + full_name (str): Role's full name. + exists (bool): Indicates if a role exists or not. + members (set): Set of current role's members. + """ + def __init__(self, module, cursor, name, server): + self.module = module + self.cursor = cursor + self.name = name + self.server = server + self.is_mariadb = self.server.is_mariadb() + + if self.is_mariadb: + self.q_builder = MariaDBQueryBuilder(self.name) + self.role_impl = MariaDBRoleImpl(self.module, self.cursor, self.name) + self.full_name = '`%s`' % self.name + self.host = '' + else: + self.host = '%' + self.q_builder = MySQLQueryBuilder(self.name, self.host) + self.role_impl = MySQLRoleImpl(self.module, self.cursor, self.name, self.host) + self.full_name = '`%s`@`%s`' % (self.name, self.host) + + self.exists = self.__role_exists() + self.members = set() + + if self.exists: + self.members = self.__get_members() + + def __role_exists(self): + """Check if a role exists. + + Returns: + bool: True if the role exists, False if it does not. + """ + self.cursor.execute(*self.q_builder.role_exists()) + return self.cursor.fetchone()[0] > 0 + + def add(self, users, privs, check_mode=False, admin=False, + set_default_role_all=True): + """Add a role. + + Args: + users (list): Role members. + privs (str): String containing privileges. + check_mode (bool): If True, just checks and does nothing. + admin (tuple): Role's admin. Contains (username, hostname). + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + if check_mode: + if not self.exists: + return True + return False + + self.cursor.execute(*self.q_builder.role_create(admin)) + + if users: + self.update_members(users, set_default_role_all=set_default_role_all) + + if privs: + for db_table, priv in iteritems(privs): + privileges_grant(self.cursor, self.name, self.host, + db_table, priv, tls_requires=None, + maria_role=self.is_mariadb) + + return True + + def drop(self, check_mode=False): + """Drop a role. + + Args: + check_mode (bool): If True, just checks and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not self.exists: + return False + + if check_mode and self.exists: + return True + + self.cursor.execute('DROP ROLE %s', (self.name,)) + return True + + def update_members(self, users, check_mode=False, append_members=False, + set_default_role_all=True): + """Add users to a role. + + Args: + users (list): Role members. + check_mode (bool): If True, just checks and does nothing. + append_members (bool): If True, adds new members passed through users + not touching current members. + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not users: + return False + + changed = False + for user in users: + if user not in self.members: + if check_mode: + return True + + self.cursor.execute(*self.q_builder.role_grant(user)) + + if set_default_role_all: + self.role_impl.set_default_role_all(user) + + changed = True + + if append_members: + return changed + + for user in self.members: + if user not in users and user != ('root', 'localhost'): + changed = self.__remove_member(user, check_mode) + + return changed + + def remove_members(self, users, check_mode=False): + """Remove members from a role. + + Args: + users (list): Role members. + check_mode (bool): If True, just checks and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not users: + return False + + changed = False + for user in users: + if user in self.members: + changed = self.__remove_member(user, check_mode) + + return changed + + def __remove_member(self, user, check_mode=False): + """Remove a member from a role. + + Args: + user (str): Role member to remove. + check_mode (bool): If True, just returns True and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if check_mode: + return True + + self.cursor.execute(*self.q_builder.role_revoke(user)) + + return True + + def update(self, users, privs, check_mode=False, + append_privs=False, subtract_privs=False, + append_members=False, detach_members=False, + admin=False, set_default_role_all=True): + """Update a role. + + Update a role if needed. + + Todo: Implement changing of role's admin when ALTER ROLE statement + to do that gets supported. + + Args: + users (list): Role members. + privs (str): String containing privileges. + check_mode (bool): If True, just checks and does nothing. + append_privs (bool): If True, adds new privileges passed through privs + not touching current privileges. + subtract_privs (bool): If True, revoke the privileges passed through privs + not touching other existing privileges. + append_members (bool): If True, adds new members passed through users + not touching current members. + detach_members (bool): If True, removes members passed through users from a role. + admin (tuple): Role's admin. Contains (username, hostname). + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + changed = False + members_changed = False + + if users: + if detach_members: + members_changed = self.remove_members(users, check_mode=check_mode) + + else: + members_changed = self.update_members(users, check_mode=check_mode, + append_members=append_members, + set_default_role_all=set_default_role_all) + + if privs: + result = user_mod(self.cursor, self.name, self.host, + None, None, None, None, None, None, + privs, append_privs, subtract_privs, None, + self.module, role=True, maria_role=self.is_mariadb) + changed = result['changed'] + + if admin: + self.role_impl.set_admin(admin) + + changed = changed or members_changed + + return changed + + def __get_members(self): + """Get current role's members. + + Returns: + set: Members. + """ + if self.is_mariadb: + self.cursor.execute('select user, host from mysql.roles_mapping where role = %s', (self.name,)) + else: + self.cursor.execute('select TO_USER as user, TO_HOST as host from mysql.role_edges where FROM_USER = %s', (self.name,)) + return set(self.cursor.fetchall()) + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + admin=dict(type='str'), + priv=dict(type='raw'), + append_privs=dict(type='bool', default=False), + subtract_privs=dict(type='bool', default=False), + members=dict(type='list', elements='str'), + append_members=dict(type='bool', default=False), + detach_members=dict(type='bool', default=False), + check_implicit_admin=dict(type='bool', default=False), + set_default_role_all=dict(type='bool', default=True), + members_must_exist=dict(type='bool', default=True) + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=( + ('append_members', 'detach_members'), + ('admin', 'members'), + ('admin', 'append_members'), + ('admin', 'detach_members'), + ('append_privs', 'subtract_privs'), + ), + ) + + login_user = module.params['login_user'] + login_password = module.params['login_password'] + name = module.params['name'] + state = module.params['state'] + admin = module.params['admin'] + priv = module.params['priv'] + check_implicit_admin = module.params['check_implicit_admin'] + connect_timeout = module.params['connect_timeout'] + config_file = module.params['config_file'] + append_privs = module.params['append_privs'] + subtract_privs = module.boolean(module.params['subtract_privs']) + members = module.params['members'] + append_members = module.params['append_members'] + detach_members = module.params['detach_members'] + ssl_cert = module.params['client_cert'] + ssl_key = module.params['client_key'] + ssl_ca = module.params['ca_cert'] + check_hostname = module.params['check_hostname'] + db = '' + set_default_role_all = module.params['set_default_role_all'] + members_must_exist = module.params['members_must_exist'] + + if priv and not isinstance(priv, (str, dict)): + msg = ('The "priv" parameter must be str or dict ' + 'but %s was passed' % type(priv)) + module.fail_json(msg=msg) + + if priv and isinstance(priv, dict): + priv = convert_priv_dict_to_str(priv) + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + + cursor = None + try: + if check_implicit_admin: + try: + cursor, db_conn = mysql_connect(module, 'root', '', config_file, + ssl_cert, ssl_key, ssl_ca, db, + connect_timeout=connect_timeout, + check_hostname=check_hostname, + autocommit=True) + except Exception: + pass + + if not cursor: + cursor, db_conn = mysql_connect(module, login_user, login_password, + config_file, ssl_cert, ssl_key, + ssl_ca, db, connect_timeout=connect_timeout, + check_hostname=check_hostname, + autocommit=True) + + except Exception as e: + module.fail_json(msg='unable to connect to database, ' + 'check login_user and login_password ' + 'are correct or %s has the credentials. ' + 'Exception message: %s' % (config_file, to_native(e))) + + # Set defaults + changed = False + + get_impl(cursor) + + if priv is not None: + try: + mode = get_mode(cursor) + except Exception as e: + module.fail_json(msg=to_native(e)) + + try: + priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs) + except Exception as e: + module.fail_json(msg='Invalid privileges string: %s' % to_native(e)) + + server = DbServer(module, cursor) + + # Check if the server supports roles + if not server.supports_roles(): + msg = ('Roles are not supported by the server. ' + 'Minimal versions are MySQL 8.0.0 or MariaDB 10.0.5.') + module.fail_json(msg=msg) + + if admin: + if not server.is_mariadb(): + module.fail_json(msg='The "admin" option can be used only with MariaDB.') + + admin = normalize_users(module, [admin])[0] + server.check_users_in_db([admin]) + + if members: + members = normalize_users(module, members, server.is_mariadb()) + if members_must_exist: + server.check_users_in_db(members) + else: + members = list(server.filter_existing_users(members)) + + # Main job starts here + role = Role(module, cursor, name, server) + + try: + if state == 'present': + if not role.exists: + if subtract_privs: + priv = None # avoid granting unwanted privileges + if detach_members: + members = None # avoid adding unwanted members + changed = role.add(members, priv, module.check_mode, admin, + set_default_role_all) + + else: + changed = role.update(members, priv, module.check_mode, append_privs, subtract_privs, + append_members, detach_members, admin, + set_default_role_all) + + elif state == 'absent': + changed = role.drop(module.check_mode) + + except Exception as e: + module.fail_json(msg=to_native(e)) + + module.exit_json(changed=changed) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_user.py b/ansible_collections/community/mysql/plugins/modules/mysql_user.py new file mode 100644 index 000000000..e87fe12db --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_user.py @@ -0,0 +1,528 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com> +# Sponsored by Four Kitchens http://fourkitchens.com. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_user +short_description: Adds or removes a user from a MySQL database +description: + - Adds or removes a user from a MySQL database. +options: + name: + description: + - Name of the user (role) to add or remove. + type: str + required: true + password: + description: + - Set the user's password. Only for C(mysql_native_password) authentication. + For other authentication plugins see the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string). + type: str + encrypted: + description: + - Indicate that the 'password' field is a `mysql_native_password` hash. + type: bool + default: false + host: + description: + - The 'host' part of the MySQL username. + type: str + default: localhost + host_all: + description: + - Override the host option, making ansible apply changes + to all hostnames for a given user. + - This option cannot be used when creating users. + type: bool + default: false + priv: + description: + - "MySQL privileges string in the format: C(db.table:priv1,priv2)." + - "Multiple privileges can be specified by separating each one using + a forward slash: C(db.table1:priv/db.table2:priv)." + - The format is based on MySQL C(GRANT) statement. + - Database and table names can be quoted, MySQL-style. + - If column privileges are used, the C(priv1,priv2) part must be + exactly as returned by a C(SHOW GRANT) statement. If not followed, + the module will always report changes. It includes grouping columns + by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))). + - Can be passed as a dictionary (see the examples). + - Supports GRANTs for procedures and functions (see the examples). + - "Note: If you pass the same C(db.table) combination to this parameter + two or more times with different privileges, + for example, C('*.*:SELECT/*.*:SHOW VIEW'), only the last one will be applied, + in this example, it will be C(SHOW VIEW) respectively. + Use C('*.*:SELECT,SHOW VIEW') instead to apply both." + type: raw + append_privs: + description: + - Append the privileges defined by priv to the existing ones for this + user instead of overwriting existing ones. Mutually exclusive with I(subtract_privs). + type: bool + default: false + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + If set, invalid privileges in I(priv) are ignored. + Mutually exclusive with I(append_privs). + version_added: '3.2.0' + type: bool + default: false + tls_requires: + description: + - Set requirement for secure transport as a dictionary of requirements (see the examples). + - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER. + - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509. + - U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls). + type: dict + version_added: 1.0.0 + sql_log_bin: + description: + - Whether binary logging should be enabled or disabled for the connection. + type: bool + default: true + force_context: + description: + - Sets the С(mysql) system database as context for the executed statements (it will be used + as a database to connect to). Useful if you use binlog / replication filters in MySQL as + per default the statements can not be caught by a binlog / replication filter, they require + a database to be set to work, otherwise the replication can break down. + - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#option_mysqld_binlog-ignore-db) + for a description on how binlog filters work (filtering on the primary). + - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#option_mysqld_replicate-ignore-db) + for a description on how replication filters work (filtering on the replica). + type: bool + default: false + version_added: '3.1.0' + state: + description: + - Whether the user should exist. + - When C(absent), removes the user. + type: str + choices: [ absent, present ] + default: present + check_implicit_admin: + description: + - Check if mysql allows login as root/nopassword before trying supplied credentials. + - If success, passed I(login_user)/I(login_password) will be ignored. + type: bool + default: false + update_password: + description: + - C(always) will update passwords if they differ. This affects I(password) and the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string). + - C(on_create) will only set the password or the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string) for newly created users. + - "C(on_new_username) works like C(on_create), but it tries to reuse an existing password: If one different user + with the same username exists, or multiple different users with the same username and equal C(plugin) and + C(authentication_string) attribute, the existing C(plugin) and C(authentication_string) are used for the + new user instead of the I(password), I(plugin), I(plugin_hash_string) or I(plugin_auth_string) argument." + type: str + choices: [ always, on_create, on_new_username ] + default: always + plugin: + description: + - User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``). + type: str + version_added: '0.1.0' + plugin_hash_string: + description: + - User's plugin hash string (``CREATE USER user IDENTIFIED WITH plugin AS plugin_hash_string``). + type: str + version_added: '0.1.0' + plugin_auth_string: + description: + - User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``). + - If I(plugin) is ``pam`` (MariaDB) or ``auth_pam`` (MySQL) an optional I(plugin_auth_string) can be used to choose a specific PAM service. + type: str + version_added: '0.1.0' + resource_limits: + description: + - Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2. + - "Available options are C(MAX_QUERIES_PER_HOUR: num), C(MAX_UPDATES_PER_HOUR: num), + C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num), C(MAX_STATEMENT_TIME: num) (supported only for MariaDB since collection version 3.7.0)." + - Used when I(state=present), ignored otherwise. + type: dict + version_added: '0.1.0' + session_vars: + description: + - "Dictionary of session variables in form of C(variable: value) to set at the beginning of module execution." + - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead. + type: dict + version_added: '3.6.0' + +notes: + - "MySQL server installs with default I(login_user) of C(root) and no password. + To secure this user as part of an idempotent playbook, you must create at least two tasks: + 1) change the root user's password, without providing any I(login_user)/I(login_password) details, + 2) drop a C(~/.my.cnf) file containing the new root credentials. + Subsequent runs of the playbook will then succeed by reading the new credentials from the file." + - Currently, there is only support for the C(mysql_native_password) encrypted password hash module. + - Supports (check_mode). + +seealso: +- module: community.mysql.mysql_info +- name: MySQL access control and account management reference + description: Complete reference of the MySQL access control and account management documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/access-control.html +- name: MySQL provided privileges reference + description: Complete reference of the MySQL provided privileges documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html + +author: +- Jonathan Mainguy (@Jmainguy) +- Benjamin Malynovytch (@bmalynovytch) +- Lukasz Tomaszkiewicz (@tomaszkiewicz) +extends_documentation_fragment: +- community.mysql.mysql + +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Removes anonymous user account for localhost + community.mysql.mysql_user: + name: '' + host: localhost + state: absent + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Removes all anonymous user accounts + community.mysql.mysql_user: + name: '' + host_all: true + state: absent + +- name: Create database user with name 'bob' and password '12345' with all database privileges + community.mysql.mysql_user: + name: bob + password: 12345 + priv: '*.*:ALL' + state: present + +- name: Create database user using hashed password with all database privileges + community.mysql.mysql_user: + name: bob + password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4' + encrypted: true + priv: '*.*:ALL' + state: present + +# Set session var wsrep_on=off before creating the user +- name: Create database user with password and all database privileges and 'WITH GRANT OPTION' + community.mysql.mysql_user: + name: bob + password: 12345 + priv: '*.*:ALL,GRANT' + state: present + session_vars: + wsrep_on: off + +- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2 + community.mysql.mysql_user: + state: present + name: bob + password: 12345dd + priv: + 'db1.*': 'ALL,GRANT' + 'db2.*': 'ALL,GRANT' + +# Use 'PROCEDURE' instead of 'FUNCTION' to apply GRANTs for a MySQL procedure instead. +- name: Grant a user the right to execute a function + community.mysql.mysql_user: + name: readonly + password: 12345 + priv: + FUNCTION my_db.my_function: EXECUTE + state: present + +- name: Modify user to require TLS connection with a valid client certificate + community.mysql.mysql_user: + name: bob + tls_requires: + x509: + state: present + +- name: Modify user to require TLS connection with a specific client certificate and cipher + community.mysql.mysql_user: + name: bob + tls_requires: + subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + cipher: 'ECDHE-ECDSA-AES256-SHA384' + +- name: Modify user to no longer require SSL + community.mysql.mysql_user: + name: bob + tls_requires: + +- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials + community.mysql.mysql_user: + login_user: root + login_password: 123456 + name: sally + state: absent + +# check_implicit_admin example +- name: > + Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. + If mysql allows root/nopassword login, try it without the credentials first. + If it's not allowed, pass the credentials + community.mysql.mysql_user: + check_implicit_admin: true + login_user: root + login_password: 123456 + name: sally + state: absent + +- name: Ensure no user named 'sally' exists at all + community.mysql.mysql_user: + name: sally + host_all: true + state: absent + +- name: Specify grants composed of more than one word + community.mysql.mysql_user: + name: replication + password: 12345 + priv: "*.*:REPLICATION CLIENT" + state: present + +- name: Revoke all privileges for user 'bob' and password '12345' + community.mysql.mysql_user: + name: bob + password: 12345 + priv: "*.*:USAGE" + state: present + +# Example privileges string format +# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL + +- name: Example using login_unix_socket to connect to server + community.mysql.mysql_user: + name: root + password: abc123 + login_unix_socket: /var/run/mysqld/mysqld.sock + +- name: Example of skipping binary logging while adding user 'bob' + community.mysql.mysql_user: + name: bob + password: 12345 + priv: "*.*:USAGE" + state: present + sql_log_bin: false + +- name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin' + community.mysql.mysql_user: + name: bob + plugin: AWSAuthenticationPlugin + plugin_hash_string: RDS + priv: '*.*:ALL' + state: present + +- name: Limit bob's resources to 10 queries per hour and 5 connections per hour + community.mysql.mysql_user: + name: bob + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + +- name: Ensure bob does not have the DELETE privilege + community.mysql.mysql_user: + name: bob + subtract_privs: true + priv: + 'db1.*': DELETE + +# Example .my.cnf file for setting the root password +# [client] +# user=root +# password=n<_665{vS43y +''' + +RETURN = '''#''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError +from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + mysql_driver_fail_msg, + mysql_common_argument_spec, + set_session_vars, +) +from ansible_collections.community.mysql.plugins.module_utils.user import ( + convert_priv_dict_to_str, + get_impl, + get_mode, + InvalidPrivsError, + limit_resources, + privileges_unpack, + sanitize_requires, + user_add, + user_delete, + user_exists, + user_mod, +) +from ansible.module_utils._text import to_native + + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + user=dict(type='str', required=True, aliases=['name']), + password=dict(type='str', no_log=True), + encrypted=dict(type='bool', default=False), + host=dict(type='str', default='localhost'), + host_all=dict(type="bool", default=False), + state=dict(type='str', default='present', choices=['absent', 'present']), + priv=dict(type='raw'), + tls_requires=dict(type='dict'), + append_privs=dict(type='bool', default=False), + subtract_privs=dict(type='bool', default=False), + check_implicit_admin=dict(type='bool', default=False), + update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False), + sql_log_bin=dict(type='bool', default=True), + plugin=dict(default=None, type='str'), + plugin_hash_string=dict(default=None, type='str'), + plugin_auth_string=dict(default=None, type='str'), + resource_limits=dict(type='dict'), + force_context=dict(type='bool', default=False), + session_vars=dict(type='dict'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=(('append_privs', 'subtract_privs'),) + ) + login_user = module.params["login_user"] + login_password = module.params["login_password"] + user = module.params["user"] + password = module.params["password"] + encrypted = module.boolean(module.params["encrypted"]) + host = module.params["host"].lower() + host_all = module.params["host_all"] + state = module.params["state"] + priv = module.params["priv"] + tls_requires = sanitize_requires(module.params["tls_requires"]) + check_implicit_admin = module.params["check_implicit_admin"] + connect_timeout = module.params["connect_timeout"] + config_file = module.params["config_file"] + append_privs = module.boolean(module.params["append_privs"]) + subtract_privs = module.boolean(module.params['subtract_privs']) + update_password = module.params['update_password'] + ssl_cert = module.params["client_cert"] + ssl_key = module.params["client_key"] + ssl_ca = module.params["ca_cert"] + check_hostname = module.params["check_hostname"] + db = '' + if module.params["force_context"]: + db = 'mysql' + sql_log_bin = module.params["sql_log_bin"] + plugin = module.params["plugin"] + plugin_hash_string = module.params["plugin_hash_string"] + plugin_auth_string = module.params["plugin_auth_string"] + resource_limits = module.params["resource_limits"] + session_vars = module.params["session_vars"] + + if priv and not isinstance(priv, (str, dict)): + module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv)) + + if priv and isinstance(priv, dict): + priv = convert_priv_dict_to_str(priv) + + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + + cursor = None + try: + if check_implicit_admin: + try: + cursor, db_conn = mysql_connect(module, "root", "", config_file, ssl_cert, ssl_key, ssl_ca, db, + connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True) + except Exception: + pass + + if not cursor: + cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db, + connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True) + except Exception as e: + module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " + "Exception message: %s" % (config_file, to_native(e))) + + if not sql_log_bin: + cursor.execute("SET SQL_LOG_BIN=0;") + + if session_vars: + set_session_vars(module, cursor, session_vars) + + get_impl(cursor) + + if priv is not None: + try: + mode = get_mode(cursor) + except Exception as e: + module.fail_json(msg=to_native(e)) + priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs) + password_changed = False + if state == "present": + if user_exists(cursor, user, host, host_all): + try: + if update_password == "always": + result = user_mod(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, + priv, append_privs, subtract_privs, tls_requires, module) + + else: + result = user_mod(cursor, user, host, host_all, None, encrypted, + None, None, None, + priv, append_privs, subtract_privs, tls_requires, module) + changed = result['changed'] + msg = result['msg'] + password_changed = result['password_changed'] + + except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: + module.fail_json(msg=to_native(e)) + else: + if host_all: + module.fail_json(msg="host_all parameter cannot be used when adding a user") + try: + if subtract_privs: + priv = None # avoid granting unwanted privileges + reuse_existing_password = update_password == 'on_new_username' + result = user_add(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, + priv, tls_requires, module.check_mode, reuse_existing_password) + changed = result['changed'] + password_changed = result['password_changed'] + if changed: + msg = "User added" + + except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: + module.fail_json(msg=to_native(e)) + + if resource_limits: + changed = limit_resources(module, cursor, user, host, resource_limits, module.check_mode) or changed + + elif state == "absent": + if user_exists(cursor, user, host, host_all): + changed = user_delete(cursor, user, host, host_all, module.check_mode) + msg = "User deleted" + else: + changed = False + msg = "User doesn't exist" + module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_variables.py b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py new file mode 100644 index 000000000..f404d5aab --- /dev/null +++ b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com> +# Certain parts are taken from Mark Theunissen's mysqldb module +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: mysql_variables + +short_description: Manage MySQL global variables +description: +- Query / Set MySQL variables. +author: +- Balazs Pocze (@banyek) +options: + variable: + description: + - Variable name to operate. + type: str + required: true + value: + description: + - If set, then sets variable value to this. + type: str + mode: + description: + - C(global) assigns C(value) to a global system variable which will be changed at runtime + but won't persist across server restarts. + - C(persist) assigns C(value) to a global system variable and persists it to + the mysqld-auto.cnf option file in the data directory + (the variable will survive service restarts). + - C(persist_only) persists C(value) to the mysqld-auto.cnf option file in the data directory + but without setting the global variable runtime value + (the value will be changed after the next service restart). + - Supported by MySQL 8.0 or later. + - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/set-variable.html). + type: str + choices: ['global', 'persist', 'persist_only'] + default: global + version_added: '0.1.0' + +notes: +- Does not support C(check_mode). + +seealso: +- module: community.mysql.mysql_info +- name: MySQL SET command reference + description: Complete reference of the MySQL SET command documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html + +extends_documentation_fragment: +- community.mysql.mysql +''' + +EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument +- name: Check for sync_binlog setting + community.mysql.mysql_variables: + variable: sync_binlog + login_unix_socket: /run/mysqld/mysqld.sock + +- name: Set read_only variable to 1 persistently + community.mysql.mysql_variables: + variable: read_only + value: 1 + mode: persist +''' + +RETURN = r''' +queries: + description: List of executed queries which modified DB's state. + returned: if executed + type: list + sample: ["SET GLOBAL `read_only` = 1"] + version_added: '0.1.0' +''' + +import os +import warnings +from re import match + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError, mysql_quote_identifier +from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec +from ansible.module_utils._text import to_native + +executed_queries = [] + + +def check_mysqld_auto(module, cursor, mysqlvar): + """Check variable's value in mysqld-auto.cnf.""" + query = ("SELECT VARIABLE_VALUE " + "FROM performance_schema.persisted_variables " + "WHERE VARIABLE_NAME = %s") + try: + cursor.execute(query, (mysqlvar,)) + res = cursor.fetchone() + except Exception as e: + if "Table 'performance_schema.persisted_variables' doesn't exist" in str(e): + module.fail_json(msg='Server version must be 8.0 or greater.') + + if res: + return res[0] + else: + return None + + +def typedvalue(value): + """ + Convert value to number whenever possible, return same value + otherwise. + + >>> typedvalue('3') + 3 + >>> typedvalue('3.0') + 3.0 + >>> typedvalue('foobar') + 'foobar' + + """ + try: + return int(value) + except ValueError: + pass + + try: + return float(value) + except ValueError: + pass + + return value + + +def getvariable(cursor, mysqlvar): + cursor.execute("SHOW VARIABLES WHERE Variable_name = %s", (mysqlvar,)) + mysqlvar_val = cursor.fetchall() + if len(mysqlvar_val) == 1: + return mysqlvar_val[0][1] + else: + return None + + +def setvariable(cursor, mysqlvar, value, mode='global'): + """ Set a global mysql variable to a given value + + The DB driver will handle quoting of the given value based on its + type, thus numeric strings like '3.0' or '8' are illegal, they + should be passed as numeric literals. + + """ + if mode == 'persist': + query = "SET PERSIST %s = " % mysql_quote_identifier(mysqlvar, 'vars') + elif mode == 'global': + query = "SET GLOBAL %s = " % mysql_quote_identifier(mysqlvar, 'vars') + elif mode == 'persist_only': + query = "SET PERSIST_ONLY %s = " % mysql_quote_identifier(mysqlvar, 'vars') + + try: + cursor.execute(query + "%s", (value,)) + executed_queries.append(query + "%s" % value) + cursor.fetchall() + result = True + except Exception as e: + result = to_native(e) + + return result + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + variable=dict(type='str'), + value=dict(type='str'), + mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'), + ) + + module = AnsibleModule( + argument_spec=argument_spec + ) + user = module.params["login_user"] + password = module.params["login_password"] + connect_timeout = module.params['connect_timeout'] + ssl_cert = module.params["client_cert"] + ssl_key = module.params["client_key"] + ssl_ca = module.params["ca_cert"] + check_hostname = module.params["check_hostname"] + config_file = module.params['config_file'] + db = 'mysql' + + mysqlvar = module.params["variable"] + value = module.params["value"] + mode = module.params["mode"] + + if mysqlvar is None: + module.fail_json(msg="Cannot run without variable to operate with") + if match('^[0-9A-Za-z_.]+$', mysqlvar) is None: + module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar) + if mysql_driver is None: + module.fail_json(msg=mysql_driver_fail_msg) + else: + warnings.filterwarnings('error', category=mysql_driver.Warning) + + try: + cursor, db_conn = mysql_connect(module, user, password, config_file, ssl_cert, ssl_key, ssl_ca, db, + connect_timeout=connect_timeout, check_hostname=check_hostname) + except Exception as e: + if os.path.exists(config_file): + module.fail_json(msg=("unable to connect to database, check login_user and " + "login_password are correct or %s has the credentials. " + "Exception message: %s" % (config_file, to_native(e)))) + else: + module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) + + mysqlvar_val = None + var_in_mysqld_auto_cnf = None + + mysqlvar_val = getvariable(cursor, mysqlvar) + if mysqlvar_val is None: + module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False) + + if value is None: + module.exit_json(msg=mysqlvar_val) + + if mode in ('persist', 'persist_only'): + var_in_mysqld_auto_cnf = check_mysqld_auto(module, cursor, mysqlvar) + + if mode == 'persist_only': + if var_in_mysqld_auto_cnf is None: + mysqlvar_val = False + else: + mysqlvar_val = var_in_mysqld_auto_cnf + + # Type values before using them + value_wanted = typedvalue(value) + value_actual = typedvalue(mysqlvar_val) + value_in_auto_cnf = None + if var_in_mysqld_auto_cnf is not None: + value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf) + + if value_wanted == value_actual and mode in ('global', 'persist'): + if mode == 'persist' and value_wanted == value_in_auto_cnf: + module.exit_json(msg="Variable is already set to requested value globally" + "and stored into mysqld-auto.cnf file.", changed=False) + + elif mode == 'global': + module.exit_json(msg="Variable is already set to requested value.", changed=False) + + if mode == 'persist_only' and value_wanted == value_in_auto_cnf: + module.exit_json(msg="Variable is already stored into mysqld-auto.cnf " + "with requested value.", changed=False) + + try: + result = setvariable(cursor, mysqlvar, value_wanted, mode) + except SQLParseError as e: + result = to_native(e) + + if result is True: + module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual, + changed=True, queries=executed_queries) + else: + module.fail_json(msg=result, changed=False) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/run_all_tests.py b/ansible_collections/community/mysql/run_all_tests.py new file mode 100755 index 000000000..94cf799bb --- /dev/null +++ b/ansible_collections/community/mysql/run_all_tests.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +import yaml +import os + +github_workflow_file = '.github/workflows/ansible-test-plugins.yml' + + +def read_github_workflow_file(): + with open(github_workflow_file, 'r') as gh_file: + try: + return yaml.safe_load(gh_file) + except yaml.YAMLError as exc: + print(exc) + + +def extract_value(target, dict_yaml): + for key, value in dict_yaml.items(): + if key == target: + return value + + +def extract_matrix(workflow_yaml): + jobs = extract_value('jobs', workflow_yaml) + integration = extract_value('integration', jobs) + strategy = extract_value('strategy', integration) + matrix = extract_value('matrix', strategy) + return matrix + + +def is_exclude(exclude_list, test_suite): + test_is_excluded = False + for excl in exclude_list: + match = 0 + + if 'ansible' in excl: + if excl.get('ansible') == test_suite.get('ansible'): + match += 1 + + if 'db_engine_name' in excl: + if excl.get('db_engine_name') == test_suite.get('db_engine_name'): + match += 1 + + if 'db_engine_version' in excl: + if excl.get('db_engine_version') == test_suite.get('db_engine_version'): + match += 1 + + if 'python' in excl: + if excl.get('python') == test_suite.get('python'): + match += 1 + + if 'connector_name' in excl: + if excl.get('connector_name') == test_suite.get('connector_name'): + match += 1 + + if 'connector_version' in excl: + if excl.get('connector_version') == test_suite.get('connector_version'): + match += 1 + + if match > 1: + test_is_excluded = True + return test_is_excluded + + return test_is_excluded + + +def main(): + workflow_yaml = read_github_workflow_file() + tests_matrix_yaml = extract_matrix(workflow_yaml) + + matrix = [] + exclude_list = tests_matrix_yaml.get('exclude') + for ansible in tests_matrix_yaml.get('ansible'): + for db_engine_name in tests_matrix_yaml.get('db_engine_name'): + for db_engine_version in tests_matrix_yaml.get('db_engine_version'): + for python in tests_matrix_yaml.get('python'): + for connector_name in tests_matrix_yaml.get('connector_name'): + for connector_version in tests_matrix_yaml.get('connector_version'): + test_suite = { + 'ansible': ansible, + 'db_engine_name': db_engine_name, + 'db_engine_version': db_engine_version, + 'python': python, + 'connector_name': connector_name, + 'connector_version': connector_version + } + if not is_exclude(exclude_list, test_suite): + matrix.append(test_suite) + + for tests in matrix: + a = tests.get('ansible') + dn = tests.get('db_engine_name') + dv = tests.get('db_engine_version') + p = tests.get('python') + cn = tests.get('connector_name') + cv = tests.get('connector_version') + make_cmd = ( + f'make ' + f'ansible="{a}" ' + f'db_engine_name="{dn}" ' + f'db_engine_version="{dv}" ' + f'python="{p}" ' + f'connector_name="{cn}" ' + f'connector_version="{cv}" ' + f'test-integration' + ) + print(f'Run tests for: Ansible: {a}, DB: {dn} {dv}, Python: {p}, Connector: {cn} {cv}') + os.system(make_cmd) + # TODO, allow for CTRL+C to break the loop more easily + # TODO, store the failures from this iteration + # TODO, display a summary of failures from every iterations + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/mysql/simplified_bsd.txt b/ansible_collections/community/mysql/simplified_bsd.txt new file mode 100644 index 000000000..6810e04e3 --- /dev/null +++ b/ansible_collections/community/mysql/simplified_bsd.txt @@ -0,0 +1,8 @@ +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py310-mysqlclient211/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py310-mysqlclient211/Dockerfile new file mode 100644 index 000000000..f7e9eb186 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py310-mysqlclient211/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2204-test-container:main +# ubuntu2204 comes with mariadb-client-10.6 + +# iproute2 # To grab docker network gateway address +# python3.10-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.10 \ + python3.10-dev \ + mariadb-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.1.1 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py310-pymysql102/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py310-pymysql102/Dockerfile new file mode 100644 index 000000000..afe6a77cb --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py310-pymysql102/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu2204-test-container:main +# ubuntu2204 comes with mariadb-client-10.6 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.10 \ + mariadb-client \ + iproute2 + +RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir pymysql==1.0.2 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py38-mysqlclient201/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py38-mysqlclient201/Dockerfile new file mode 100644 index 000000000..68ea3f689 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py38-mysqlclient201/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mariadb-client-10.3 + +# iproute2 # To grab docker network gateway address +# python3.8-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + python3.8-dev \ + mariadb-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py38-pymysql093/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py38-pymysql093/Dockerfile new file mode 100644 index 000000000..22c8c5779 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py38-pymysql093/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mariadb-client-10.3 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + mariadb-client \ + iproute2 + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py39-mysqlclient203/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py39-mysqlclient203/Dockerfile new file mode 100644 index 000000000..b7837b2a8 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py39-mysqlclient203/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mariadb-client-10.3 + +# iproute2 # To grab docker network gateway address +# python3.9-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.9 \ + python3.9-dev \ + mariadb-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mariadb-py39-pymysql093/Dockerfile b/ansible_collections/community/mysql/test-containers/mariadb-py39-pymysql093/Dockerfile new file mode 100644 index 000000000..a1451ff12 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mariadb-py39-pymysql093/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mariadb-client-10.3 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.9 \ + mariadb-client \ + iproute2 + +RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/my57-py38-mysqlclient201/Dockerfile b/ansible_collections/community/mysql/test-containers/my57-py38-mysqlclient201/Dockerfile new file mode 100644 index 000000000..0eb177829 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/my57-py38-mysqlclient201/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu1804-test-container:main +# ubuntu1804 comes with mysql-client-5.7 + +# iproute2 # To grab docker network gateway address +# python3.8-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + python3.8-dev \ + mysql-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/my57-py38-pymysql0711/Dockerfile b/ansible_collections/community/mysql/test-containers/my57-py38-pymysql0711/Dockerfile new file mode 100644 index 000000000..9141709dd --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/my57-py38-pymysql0711/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu1804-test-container:main +# ubuntu1804 comes with mysql-client-5.7 + +# iproute2 # To grab docker network gateway address +# python3.8-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + python3.8-dev \ + mysql-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.7.11 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/my57-py38-pymysql093/Dockerfile b/ansible_collections/community/mysql/test-containers/my57-py38-pymysql093/Dockerfile new file mode 100644 index 000000000..6b0f519ab --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/my57-py38-pymysql093/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu1804-test-container:main +# ubuntu1804 comes with mysql-client-5.7 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + mysql-client \ + iproute2 + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py310-mysqlclient211/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py310-mysqlclient211/Dockerfile new file mode 100644 index 000000000..1aea0cd19 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py310-mysqlclient211/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2204-test-container:main +# ubuntu2204 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +# python3.10-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.10 \ + python3.10-dev \ + mysql-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.1.1 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py310-pymysql102/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py310-pymysql102/Dockerfile new file mode 100644 index 000000000..871a1e4fe --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py310-pymysql102/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu2204-test-container:main +# ubuntu2204 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.10 \ + mysql-client \ + iproute2 + +RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir pymysql==1.0.2 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py38-mysqlclient201/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py38-mysqlclient201/Dockerfile new file mode 100644 index 000000000..eb835c21b --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py38-mysqlclient201/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +# python3.8-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + python3.8-dev \ + mysql-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py38-pymysql093/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py38-pymysql093/Dockerfile new file mode 100644 index 000000000..e97e5e26a --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py38-pymysql093/Dockerfile @@ -0,0 +1,15 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.8 \ + mysql-client \ + iproute2 + +RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py39-mysqlclient203/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py39-mysqlclient203/Dockerfile new file mode 100644 index 000000000..396d89571 --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py39-mysqlclient203/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +# python3.9-dev # Reqs for mysqlclient +# default-libmysqlclient-dev # Reqs for mysqlclient +# build-essential # Reqs for mysqlclient +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.9 \ + python3.9-dev \ + mysql-client \ + iproute2 \ + default-libmysqlclient-dev \ + build-essential + +RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/test-containers/mysql-py39-pymysql093/Dockerfile b/ansible_collections/community/mysql/test-containers/mysql-py39-pymysql093/Dockerfile new file mode 100644 index 000000000..57ef15e1a --- /dev/null +++ b/ansible_collections/community/mysql/test-containers/mysql-py39-pymysql093/Dockerfile @@ -0,0 +1,16 @@ +FROM quay.io/ansible/ubuntu2004-test-container:main +# ubuntu2004 comes with mysql-client-8 + +# iproute2 # To grab docker network gateway address +RUN apt-get update -y && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3.9 \ + mysql-client \ + iproute2 + +# cffi # To connect to MySQL 8 with Python3.9 and PyMySQL +RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir cffi pymysql==0.9.3 + +ENV container=docker +CMD ["/sbin/init"] diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/aliases b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/aliases new file mode 100644 index 000000000..1065935fc --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/aliases @@ -0,0 +1,7 @@ +destructive +unsupported # these tests conflict with mysql_replication tests and do not run on changes to the mysql_replication module +skip/aix +skip/osx +skip/freebsd +skip/rhel +needs/root diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/defaults/main.yml new file mode 100644 index 000000000..eb32dc1a1 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/defaults/main.yml @@ -0,0 +1,9 @@ +--- +mysql_host: "{{ gateway_addr }}" +master_port: 3306 +standby_port: 3307 +test_db: test_db +replication_user: replication_user +replication_pass: replication_pass +dump_path: /tmp/dump.sql +conn_name: master-1 diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/meta/main.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/meta/main.yml new file mode 100644 index 000000000..719861713 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: +- setup_mariadb diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/main.yml new file mode 100644 index 000000000..4ea76a9e8 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/main.yml @@ -0,0 +1,21 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Initial CI tests of mysql_replication module +- import_tasks: mariadb_replication_initial.yml + when: + - ansible_facts.distribution == 'CentOS' + - ansible_facts.distribution_major_version is version('7', '>=') + +# Tests of master_use_gtid parameter +# https://github.com/ansible/ansible/pull/62648 +- import_tasks: mariadb_master_use_gtid.yml + when: + - ansible_facts.distribution == 'CentOS' + - ansible_facts.distribution_major_version is version('7', '>=') + +# Tests of connection_name parameter +- import_tasks: mariadb_replication_connection_name.yml + when: + - ansible_facts.distribution == 'CentOS' + - ansible_facts.distribution_major_version is version('7', '>=') diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml new file mode 100644 index 000000000..699b61f8f --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml @@ -0,0 +1,173 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Tests for master_use_gtid parameter. +# https://github.com/ansible/ansible/pull/62648 + +############################# +# master_use_gtid: "disabled" +############################# + +# Auxiliary step: +- name: Get master status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: getmaster + register: primary_status + +# Set master_use_gtid disabled: +- name: Run replication + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: changemaster + master_host: '{{ mysql_host }}' + master_port: "{{ primary_db.port }}" + master_user: "{{ replication_user }}" + master_password: "{{ replication_pass }}" + master_log_file: mysql-bin.000001 + master_log_pos: '{{ primary_status.Position }}' + master_use_gtid: disabled + register: result + +- assert: + that: + - result is changed + +# Start standby for further tests: +- name: Start standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: startslave + +- name: Get standby status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: getslave + register: slave_status + +- assert: + that: + - slave_status.Using_Gtid == 'No' + +# Stop standby for further tests: +- name: Stop standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave + +################################ +# master_use_gtid: "current_pos" +################################ + +# Auxiliary step: +- name: Get master status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: getmaster + register: primary_status + +# Set master_use_gtid current_pos: +- name: Run replication + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: changemaster + master_host: '{{ mysql_host }}' + master_port: "{{ primary_db.port }}" + master_user: "{{ replication_user }}" + master_password: "{{ replication_pass }}" + master_log_file: mysql-bin.000001 + master_log_pos: '{{ primary_status.Position }}' + master_use_gtid: current_pos + register: result + +- assert: + that: + - result is changed + +# Start standby for further tests: +- name: Start standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: startslave + +- name: Get standby status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: getslave + register: slave_status + +- assert: + that: + - slave_status.Using_Gtid == 'Current_Pos' + +# Stop standby for further tests: +- name: Stop standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave + +############################## +# master_use_gtid: "slave_pos" +############################## + +# Auxiliary step: +- name: Get master status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: getmaster + register: primary_status + +# Set master_use_gtid slave_pos: +- name: Run replication + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: changemaster + master_host: '{{ mysql_host }}' + master_port: "{{ primary_db.port }}" + master_user: "{{ replication_user }}" + master_password: "{{ replication_pass }}" + master_log_file: mysql-bin.000001 + master_log_pos: '{{ primary_status.Position }}' + master_use_gtid: slave_pos + register: result + +- assert: + that: + - result is changed + +# Start standby for further tests: +- name: Start standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: startslave + +- name: Get standby status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: getslave + register: slave_status + +- assert: + that: + - slave_status.Using_Gtid == 'Slave_Pos' + +# Stop standby for further tests: +- name: Stop standby + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml new file mode 100644 index 000000000..3928c78c4 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml @@ -0,0 +1,118 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Needs for further tests: +- name: Stop slave + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave + +- name: Reset slave all + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: resetslaveall + +# Get master log pos: +- name: Get master status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: getmaster + register: primary_status + +# Test changemaster mode: +- name: Run replication with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: changemaster + master_host: '{{ mysql_host }}' + master_port: "{{ primary_db.port }}" + master_user: "{{ replication_user }}" + master_password: "{{ replication_pass }}" + master_log_file: mysql-bin.000001 + master_log_pos: '{{ primary_status.Position }}' + connection_name: '{{ conn_name }}' + register: result + +- assert: + that: + - result is changed + - result.queries[0] is match("CHANGE MASTER ('\S+' )?TO MASTER_HOST='[0-9.]+',MASTER_USER='\w+',MASTER_PASSWORD='[*]{8}',MASTER_PORT=\d+,MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=\d+") + +# Test startslave mode: +- name: Start slave with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: startslave + connection_name: "{{ conn_name }}" + register: result + +- assert: + that: + - result is changed + - result.queries == ["START SLAVE \'{{ conn_name }}\'"] + +# Test getslave mode: +- name: Get standby statu with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: getslave + connection_name: "{{ conn_name }}" + register: slave_status + +- assert: + that: + - slave_status.Is_Slave == true + - slave_status.Master_Host == ''{{ mysql_host }}'' + - slave_status.Exec_Master_Log_Pos == primary_status.Position + - slave_status.Master_Port == {{ primary_db.port }} + - slave_status.Last_IO_Errno == 0 + - slave_status.Last_IO_Error == '' + - slave_status is not changed + +# Test stopslave mode: +- name: Stop slave with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave + connection_name: "{{ conn_name }}" + register: result + +- assert: + that: + - result is changed + - result.queries == ["STOP SLAVE \'{{ conn_name }}\'"] + +# Test reset +- name: Reset slave with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: resetslave + connection_name: "{{ conn_name }}" + register: result + +- assert: + that: + - result is changed + - result.queries == ["RESET SLAVE \'{{ conn_name }}\'"] + +# Test reset all +- name: Reset slave all with connection_name + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: resetslaveall + connection_name: "{{ conn_name }}" + register: result + +- assert: + that: + - result is changed + - result.queries == ["RESET SLAVE \'{{ conn_name }}\' ALL"] diff --git a/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml new file mode 100644 index 000000000..f65d0902e --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml @@ -0,0 +1,96 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Preparation: +- name: Create user for replication + shell: "echo \"GRANT REPLICATION SLAVE ON *.* TO '{{ replication_user }}'@'localhost' IDENTIFIED BY '{{ replication_pass }}'; FLUSH PRIVILEGES;\" | mysql -P {{ primary_db.port }} -h '{{ mysql_host }}'" + +- name: Create test database + mysql_db: + login_host: '{{ mysql_host }}' + login_port: '{{ primary_db.port }}' + state: present + name: '{{ test_db }}' + +- name: Dump all databases from the master + shell: 'mysqldump -P {{ primary_db.port }} -h 127.0.01 --all-databases --master-data=2 > {{ dump_path }}' + +- name: Restore the dump to the replica + shell: "mysql -P {{ replica_db.port }} -h '{{ mysql_host }}' < {{ dump_path }}" + +# Test getmaster mode: +- name: Get master status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ primary_db.port }}" + mode: getmaster + register: master_status + +- assert: + that: + - master_status.Is_Master == true + - master_status.Position != 0 + - master_status is not changed + +# Test changemaster mode: +- name: Run replication + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: changemaster + master_host: '{{ mysql_host }}' + master_port: "{{ primary_db.port }}" + master_user: "{{ replication_user }}" + master_password: "{{ replication_pass }}" + master_log_file: mysql-bin.000001 + master_log_pos: '{{ master_status.Position }}' + register: result + +- assert: + that: + - result is changed + - result.queries[0] is match("CHANGE MASTER ('\S+' )?TO MASTER_HOST='[0-9.]+',MASTER_USER='\w+',MASTER_PASSWORD='[*]{8}',MASTER_PORT=\d+,MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=\d+") + +# Test startslave mode: +- name: Start slave + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: startslave + register: result + +- assert: + that: + - result is changed + - result.queries == ["START SLAVE"] + +# Test getslave mode: +- name: Get replica status + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: getslave + register: slave_status + +- assert: + that: + - slave_status.Is_Slave == true + - slave_status.Master_Host == ''{{ mysql_host }}'' + - slave_status.Exec_Master_Log_Pos == master_status.Position + - slave_status.Master_Port == {{ primary_db.port }} + - slave_status.Last_IO_Errno == 0 + - slave_status.Last_IO_Error == '' + - slave_status is not changed + +# Test stopslave mode: +- name: Stop slave + mysql_replication: + login_host: '{{ mysql_host }}' + login_port: "{{ replica_db.port }}" + mode: stopslave + register: result + +- assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/fake_root.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/fake_root.yml new file mode 100644 index 000000000..49531b8d7 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/fake_root.yml @@ -0,0 +1,11 @@ +--- + +- name: "{{ role_name }} | Fake root | Ensure folder" + ansible.builtin.file: + path: "{{ playbook_dir }}/root" + state: directory + +- name: "{{ role_name }} | Fake root | Ensure default file exists" + ansible.builtin.file: + path: "{{ playbook_dir }}/root/.my.cnf" + state: touch diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/main.yml new file mode 100644 index 000000000..0d5e36bba --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/main.yml @@ -0,0 +1,18 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Prepare the fake root folder + ansible.builtin.import_tasks: + file: fake_root.yml + +# setvars.yml requires the iproute2 package installed by install.yml +- name: Set variables + ansible.builtin.import_tasks: + file: setvars.yml + +- name: Verify all components version under test + ansible.builtin.import_tasks: + file: verify.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/setvars.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/setvars.yml new file mode 100644 index 000000000..3e070a93e --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/setvars.yml @@ -0,0 +1,79 @@ +--- + +- name: "{{ role_name }} | Setvars | Extract Podman/Docker Network Gateway" + ansible.builtin.shell: + cmd: ip route|grep default|awk '{print $3}' + register: ip_route_output + +- name: "{{ role_name }} | Setvars | Set Fact" + ansible.builtin.set_fact: + gateway_addr: "{{ ip_route_output.stdout }}" + connector_name_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/connector_name' + ) }} + connector_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/connector_version' + ) }} + db_engine_name_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/db_engine_name' + ) }} + db_engine_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/db_engine_version' + ) }} + python_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/python' + ) }} + ansible_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/ansible' + ) }} + +- name: "{{ role_name }} | Setvars | Set Fact using above facts" + ansible.builtin.set_fact: + connector_name: "{{ connector_name_lookup.strip() }}" + connector_version: "{{ connector_version_lookup.strip() }}" + db_engine: "{{ db_engine_name_lookup.strip() }}" + db_version: "{{ db_engine_version_lookup.strip() }}" + python_version: "{{ python_version_lookup.strip() }}" + test_ansible_version: >- + {%- if ansible_version_lookup == 'devel' -%} + {{ ansible_version_lookup }} + {%- else -%} + {{ ansible_version_lookup.split('-')[1].strip() }} + {%- endif -%} + mysql_command: >- + mysql + -h{{ gateway_addr }} + -P{{ mysql_primary_port }} + -u{{ mysql_user }} + -p{{ mysql_password }} + --protocol=tcp + mysql_command_wo_port: >- + mysql + -h{{ gateway_addr }} + -u{{ mysql_user }} + -p{{ mysql_password }} + --protocol=tcp + +- name: "{{ role_name }} | Setvars | Output test informations" + vars: + msg: |- + connector_name: {{ connector_name }} + connector_version: {{ connector_version }} + db_engine: {{ db_engine }} + db_version: {{ db_version }} + python_version: {{ python_version }} + test_ansible_version: {{ test_ansible_version }} + ansible.builtin.debug: + msg: "{{ msg.split('\n') }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/verify.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/verify.yml new file mode 100644 index 000000000..74aa0f26e --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_controller/tasks/verify.yml @@ -0,0 +1,59 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: root + login_password: msandbox + login_host: "{{ gateway_addr }}" + login_port: 3307 + + block: + + - name: Query Primary container over TCP for MySQL/MariaDB version + mysql_info: + <<: *mysql_params + filter: + - version + register: primary_info + + - name: Assert that test container runs the expected MySQL/MariaDB version + assert: + that: + - "'{{ primary_info.version.major }}.{{ primary_info.version.minor }}\ + .{{ primary_info.version.release }}' == '{{ db_version }}'" + + - name: Assert that mysql_info module used the expected version of pymysql + assert: + that: + - primary_info.connector_name == connector_name + - primary_info.connector_version == connector_version + when: + - connector_name == 'pymysql' + + - name: Assert that mysql_info module used the expected version of mysqlclient + assert: + that: + - primary_info.connector_name == 'MySQLdb' + - primary_info.connector_version == connector_version + when: + - connector_name == 'mysqlclient' + + - name: Display the python version in use + command: + cmd: python{{ python_version }} -V + changed_when: false + register: python_in_use + + - name: Assert that expected Python is installed + assert: + that: + - python_in_use.stdout is search(python_version) + + - name: Assert that we run the expected ansible version + assert: + that: + - > + "{{ ansible_version.major }}.{{ ansible_version.minor }}" + is version(test_ansible_version, '==') + when: + - test_ansible_version != 'devel' # Devel will change overtime diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml new file mode 100644 index 000000000..39f3239c7 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml @@ -0,0 +1,9 @@ +- name: delete temporary directory + include_tasks: default-cleanup.yml + tags: + - setup_remote_tmp_dir + +- name: delete temporary directory (windows) + include_tasks: windows-cleanup.yml + tags: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml new file mode 100644 index 000000000..39872d749 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml @@ -0,0 +1,5 @@ +- name: delete temporary directory + file: + path: "{{ remote_tmp_dir }}" + state: absent + no_log: yes diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml new file mode 100644 index 000000000..1e0f51b89 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml @@ -0,0 +1,11 @@ +- name: create temporary directory + tempfile: + state: directory + suffix: .test + register: remote_tmp_dir + notify: + - delete temporary directory + +- name: record temporary directory + set_fact: + remote_tmp_dir: "{{ remote_tmp_dir.path }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml new file mode 100644 index 000000000..5d898abe5 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml @@ -0,0 +1,19 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: make sure we have the ansible_os_family and ansible_distribution_version facts + setup: + gather_subset: distribution + when: ansible_facts == {} + tags: + - setup_remote_tmp_dir + +- include_tasks: "{{ lookup('first_found', files)}}" + vars: + files: + - "{{ ansible_os_family | lower }}.yml" + - "default.yml" + tags: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/defaults/main.yml new file mode 100644 index 000000000..30ac858c0 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/defaults/main.yml @@ -0,0 +1,28 @@ +--- +# defaults file for test_mysql_db +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 + +# Database names +db_names: + - "data" + - "db%" + +# Database formats +db_formats: + - { format_type: "sql", file: "dbdata.sql", format_msg_type: "ASCII", file2: "dump2.sql", file3: "dump3.sql", file4: "dump4.sql" } + - { format_type: "gz", file: "dbdata.gz", format_msg_type: "gzip", file2: "dump2.gz", file3: "dump3.gz", file4: "dump4.gz" } + - { format_type: "bz2", file: "dbdata.bz2", format_msg_type: "bzip2", file2: "dump2.bz2", file3: "dump3.bz2", file4: "dump4.bz2" } + +db_name2: 'data2' +db_user1: 'datauser1' +db_user2: 'datauser2' + +tmp_dir: '/tmp' +db_latin1_name: 'db_latin1' +file4: 'latin1_file' + +user_name_1: 'db_user1' +user_password_1: 'gadfFDSdtTU^Sdfuj' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/meta/main.yml new file mode 100644 index 000000000..aebda436b --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_controller diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml new file mode 100644 index 000000000..390c6ae78 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml @@ -0,0 +1,123 @@ +--- +- name: Config overrides | Set facts + set_fact: + db_to_create: testdb1 + config_file: "{{ playbook_dir }}/.my1.cnf" + fake_port: 9999 + fake_host: "blahblah.local" + include_dir: "{{ playbook_dir }}/mycnf.d" + +- name: Config overrides | Create custom config file + shell: 'echo "[client]" > {{ config_file }}' + +- name: Config overrides | Add fake port to config file + shell: 'echo "port = {{ fake_port }}" >> {{ config_file }}' + +- name: Config overrides | Add blank line + shell: 'echo "" >> {{ config_file }}' + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) + +- name: Config overrides | Create include_dir + file: + path: '{{ include_dir }}' + state: directory + mode: '0777' + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) + +- name: Config overrides | Add include_dir + lineinfile: + path: '{{ config_file }}' + line: '!includedir {{ include_dir }}' + insertafter: EOF + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) + +- name: Config overrides | Create database using fake port to connect to, must fail + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_to_create }}' + state: present + check_implicit_admin: yes + config_file: '{{ config_file }}' + config_overrides_defaults: yes + ignore_errors: yes + register: result + +- name: Config overrides | Must fail because login_port default has beed overriden by wrong value from config file + assert: + that: + - result is failed + - result.msg is search("unable to connect to database") + +- name: Config overrides | Create database using default port + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_to_create }}' + state: present + check_implicit_admin: yes + config_file: '{{ config_file }}' + config_overrides_defaults: no + register: result + +- name: Config overrides | Must not fail because of the default of login_port is correct + assert: + that: + - result is changed + +- name: Config overrides | Reinit custom config file + shell: 'echo "[client]" > {{ config_file }}' + +- name: Config overrides | Add fake host to config file + shell: 'echo "host = {{ fake_host }}" >> {{ config_file }}' + +- name: Config overrides | Remove database using fake login_host + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_to_create }}' + state: absent + config_file: '{{ config_file }}' + config_overrides_defaults: yes + register: result + ignore_errors: yes + +- name: Config overrides | Must fail because login_host default has beed overriden by wrong value from config file + assert: + that: + - result is failed + - result.msg is search("Can't connect to MySQL server on '{{ fake_host }}'") or result.msg is search("Unknown MySQL server host '{{ fake_host }}'") + +- name: Config overrides | Clean up test database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_to_create }}' + state: absent + check_implicit_admin: yes diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml new file mode 100644 index 000000000..02e5df2c2 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml @@ -0,0 +1,105 @@ +--- + +- name: Encoding | Set fact + set_fact: + latin1_file1: "{{ tmp_dir }}/{{ file }}" + +- name: Deleting Latin1 encoded Database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_latin1_name }}' + state: absent + +- name: Encoding | Create Latin1 encoded database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_latin1_name }}' + state: present + encoding: latin1 + +- name: Encoding | Create a table in Latin1 database + command: "{{ mysql_command }} {{ db_latin1_name }} -e \"create table testlatin1(id int, name varchar(100))\"" + + +# Inserting a string in latin1 into table, , this string be tested later, +# so report any change of content in the test too +- name: Encoding | Inserting data into Latin1 database + command: "{{ mysql_command }} {{ db_latin1_name }} -e \"insert into testlatin1 value(47,'Amédée Bôlüt')\"" + +- name: Encoding | Selecting table + command: "{{ mysql_command }} {{ db_latin1_name }} -e \"select * from testlatin1\"" + register: output + +- name: Encoding | Dumping a table in Latin1 database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: "{{ db_latin1_name }}" + encoding: latin1 + target: "{{ latin1_file1 }}" + state: dump + register: result + +- assert: + that: + - result is changed + +- name: Encoding | State dump - file name should exist (latin1_file1) + file: + name: '{{ latin1_file1 }}' + state: file + +- name: od the file and check of latin1 encoded string is present + shell: grep -a 47 {{ latin1_file1 }} | od -c |grep "A m 351 d 351 e B 364\|A m 303 251 d 303 251 e B 303" + +- name: Encoding | Dropping {{ db_latin1_name }} database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_latin1_name }}' + state: absent + +- name: Encoding | Importing the latin1 mysql script + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + state: import + encoding: latin1 + name: '{{ db_latin1_name }}' + target: "{{ latin1_file1 }}" + register: result + +- name: Encoding | Assert that importing latin1 is changed + assert: + that: + - result is changed + +- name: Encoding | Check encoding of table + ansible.builtin.command: + cmd: > + {{ mysql_command }} + {{ db_latin1_name }} + -e "SHOW FULL COLUMNS FROM {{ db_latin1_name }}.testlatin1" + register: output + failed_when: '"latin1_swedish_ci" not in output.stdout' + +- name: Encoding | Clean up database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_latin1_name }}' + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue-28.yml new file mode 100644 index 000000000..8cad28e69 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue-28.yml @@ -0,0 +1,88 @@ +--- +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + - name: create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: "%" + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + tls_requires: + SSL: + + - name: attempt connection with newly created user (expect failure) + mysql_db: + name: '{{ db_name }}' + state: absent + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: attempt connection with newly created user ignoring hostname + mysql_db: + name: '{{ db_name }}' + state: absent + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: no + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml new file mode 100644 index 000000000..ea1768ad1 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml @@ -0,0 +1,149 @@ +--- + +# When mysqldump encountered an issue, mysql_db was still happy. But the +# dump produced was empty or worse, only contained `DROP TABLE IF EXISTS...` + +- module_defaults: + community.mysql.mysql_db: &mysql_defaults + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + community.mysql.mysql_query: *mysql_defaults + + block: + + - name: Dumps errors | Setup test | Create 2 schemas + community.mysql.mysql_db: + name: + - "db1" + - "db2" + state: present + + - name: Dumps errors | Setup test | Create 2 tables + community.mysql.mysql_query: + query: + - "CREATE TABLE db1.t1 (id int)" + - "CREATE TABLE db1.t2 (id int)" + - "CREATE VIEW db2.v1 AS SELECT id from db1.t1" + + - name: Dumps errors | Full dump without compression + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump.sql + register: full_dump + + - name: Dumps errors | Full dump with gunzip + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump.sql.gz + register: full_dump_gz + + - name: Dumps errors | Distinct dump without compression + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2.sql + register: dump_db2 + + - name: Dumps errors | Distinct dump with gunzip + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2.sql.gz + register: dump_db2_gz + + - name: Dumps errors | Check distinct dumps are changed + ansible.builtin.assert: + that: + - dump_db2 is changed + - dump_db2_gz is changed + + # Now db2.v1 targets an inexistant table so mysqldump will fail + - name: Dumps errors | Drop t1 + community.mysql.mysql_query: + query: + - "DROP TABLE db1.t1" + + - name: Dumps errors | Full dump after drop t1 without compression + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql + pipefail: true # This should do nothing + + register: full_dump_without_t1 + ignore_errors: true + + - name: Dumps errors | Full dump after drop t1 with gzip without the fix + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql.gz + register: full_dump_without_t1_gz_without_fix + ignore_errors: true + + - name: Dumps errors | Full dump after drop t1 with gzip with the fix + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql.gz + pipefail: true + register: full_dump_without_t1_gz_with_fix + ignore_errors: true + + - name: Dumps errors | Check full dump + ansible.builtin.assert: + that: + - full_dump_without_t1 is failed + - full_dump_without_t1.msg is search( + 'references invalid table') + - full_dump_without_t1_gz_without_fix is changed + - full_dump_without_t1_gz_with_fix is failed + - full_dump_without_t1_gz_with_fix.msg is search( + 'references invalid table') + + - name: Dumps errors | Distinct dump after drop t1 without compression + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql + pipefail: true # This should do nothing + register: dump_db2_without_t1 + ignore_errors: true + + - name: Dumps errors | Distinct dump after drop t1 with gzip without the fix + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql.gz + register: dump_db2_without_t1_gz_without_fix + ignore_errors: true + + - name: Dumps errors | Distinct dump after drop t1 with gzip with the fix + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql.gz + pipefail: true + register: dump_db2_without_t1_gz_with_fix + ignore_errors: true + + - name: Dumps errors | Check distinct dump + ansible.builtin.assert: + that: + - dump_db2_without_t1 is failed + - dump_db2_without_t1.msg is search( + 'references invalid table') + - dump_db2_without_t1_gz_without_fix is changed + - dump_db2_without_t1_gz_with_fix is failed + - dump_db2_without_t1_gz_with_fix.msg is search( + 'references invalid table') + - name: Dumps errors | Cleanup + community.mysql.mysql_db: + name: + - "db1" + - "db2" + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/main.yml new file mode 100644 index 000000000..544ad4d60 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/main.yml @@ -0,0 +1,65 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# test code for the mysql_db module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: Check state present/absent + include_tasks: state_present_absent.yml + vars: + db_name: "{{ item }}" + loop: "{{ db_names }}" + +- name: Check state dump/import + include_tasks: state_dump_import.yml + vars: + db_name: "{{ item.0 }}" + file: "{{ item.1.file }}" + file2: "{{ item.1.file2 }}" + file3: "{{ item.1.file3 }}" + file4: "{{ item.1.file4 }}" + format_msg_type: "{{ item.1.format_msg_type }}" + format_type: "{{ item.1.format_type }}" + with_nested: + - "{{ db_names }}" + - "{{ db_formats }}" + +- name: Check state present/absent with multiple databases + include_tasks: multi_db_create_delete.yml + +- name: Check state dump/import with encoding + include_tasks: encoding_dump_import.yml + vars: + file: "latin1.sql" + format_msg_type: "ASCII" + +- name: Check MySQL config file + include_tasks: config_overrides_defaults.yml + when: ansible_python.version_info[0] >= 3 + +- name: Check issue 28 + include_tasks: issue-28.yml + vars: + db_name: "{{ item }}" + loop: "{{ db_names }}" + +- name: Check errors from mysqldump are seen issue 256 + ansible.builtin.include_tasks: issue_256_mysqldump_errors.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml new file mode 100644 index 000000000..0bd7d5870 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml @@ -0,0 +1,639 @@ +# Copyright (c) 2019, Pratik Gadiya <pratikgadiya1@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- set_fact: + db1_name: "database1" + db2_name: "database2" + db3_name: "database3" + db4_name: "database4" + db5_name: "database5" + dump1_file: "/tmp/dump1_file.sql" + dump2_file: "/tmp/all.sql" + +# ============================== CREATE TEST =============================== +# +# ========================================================================== +# Initial check - To confirm that database does not exist before executing check mode tasks +- name: Drop databases before test + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: absent + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does not exist + assert: + that: + - "'{{ db1_name }}' not in mysql_result.stdout" + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' not in mysql_result.stdout" + +# ========================================================================== +# Create multiple databases that does not exists (check mode) +- name: Create multiple databases that does not exists (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: check_mode_result + check_mode: yes + +- name: assert successful completion of create database using check_mode since databases does not exist prior + assert: + that: + - check_mode_result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does not exist (since created via check mode) + assert: + that: + - "'{{ db1_name }}' not in mysql_result.stdout" + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' not in mysql_result.stdout" + +# ========================================================================== +# Create multiple databases +- name: Create multiple databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: result + +- name: assert successful completion of create database + assert: + that: + - result is changed + - result.db_list == ['{{ db1_name }}', '{{ db2_name }}', '{{ db3_name }}'] + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist after creation + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================= +# Recreate already existing databases (check mode) +- name: Recreate already existing databases (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: check_mode_result + check_mode: yes + +- name: assert that recreation of existing databases does not make change (since recreated using check mode) + assert: + that: + - check_mode_result is not changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist (since performed recreation of existing databases via check mode) + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================== +# Recreate same databases +- name: Recreate multiple databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: result + +- name: assert that recreation of existing databases does not make change + assert: + that: + - result is not changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does priorly exist + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================== +# Delete one of the databases (db2 here) +- name: Delete db2 database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db2_name }}' + state: absent + register: result + +- name: assert successful completion of deleting database + assert: + that: + - result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that only db2 database does not exist + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================= +# Recreate multiple databases in which few databases does not exists (check mode) +- name: Recreate multiple databases in which few databases does not exists (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: check_mode_result + check_mode: yes + +- name: assert successful completion of recreation of partially existing database using check mode + assert: + that: + - check_mode_result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that recreated non existing databases does not exist (since created via check mode) + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================== +# Create multiple databases +- name: Create multiple databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: present + register: result + +- name: assert successful completion of create database + assert: + that: + - result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ============================== DUMP TEST ================================= +# +# ========================================================================== +# Check that dump file does not exist +- name: Dump file does not exist + file: + name: '{{ dump1_file }}' + state: absent + +# ========================================================================== +# Dump existing databases (check mode) +- name: Dump existing databases (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db3_name }}' + state: dump + target: '{{ dump1_file }}' + register: check_mode_dump_result + check_mode: yes + +- name: assert successful completion of dump operation using check mode + assert: + that: + - check_mode_dump_result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist (check mode) + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +- name: state dump - file name should not exist (since dumped via check mode) + file: + name: '{{ dump1_file }}' + state: absent + +# ========================================================================== +# Dump existing and non-existing databases (check mode) +- name: Dump existing and non-existing databases (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - "{{ db1_name }}" + - "{{ db4_name }}" + - "{{ db3_name }}" + state: dump + target: "{{ dump1_file }}" + register: check_mode_dump_result + ignore_errors: True + check_mode: yes + +- name: assert that dump operation of existing and non existing databases does not make change (using check mode) + assert: + that: + - "'Cannot dump database' in check_mode_dump_result['msg']" + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist (since check mode) + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + +- name: state dump - file name should not exist (since prior dump operation performed via check mode) + file: + name: '{{ dump1_file }}' + state: absent + +# ========================================================================== +# Dump non-existing databases (check mode) +- name: Dump non-existing databases (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - "{{ db4_name }}" + - "{{ db5_name }}" + state: dump + target: "{{ dump1_file }}" + register: check_mode_dump_result + ignore_errors: True + check_mode: yes + +- name: assert successful completion of dump operation using check mode + assert: + that: + - "'Cannot dump database' in check_mode_dump_result['msg']" + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist (since delete via check mode) + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + - "'{{ db5_name }}' not in mysql_result.stdout" + +- name: state dump - file name should not exist (since prior dump operation performed via check mode) + file: + name: '{{ dump1_file }}' + state: absent + +# ========================================================================== +# Dump existing databases + +- name: Dump existing databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + state: dump + target: '{{ dump1_file }}' + register: dump_result + +- name: Assert successful completion of dump operation (existing database) + assert: + that: + - dump_result is changed + - dump_result.db_list == ['{{ db1_name }}', '{{ db2_name }}', '{{ db3_name }}'] + +- name: Run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +- name: State dump - file name should exist (dump1_file) + file: + name: '{{ dump1_file }}' + state: file + +- name: Check if db1 database create command is present in the dumped file + shell: "grep -i 'CREATE DATABASE.*`{{ db1_name }}`' {{ dump1_file }}" + +- name: Check if db2 database create command is present in the dumped file + shell: "grep -i 'CREATE DATABASE.*`{{ db2_name }}`' {{ dump1_file }}" + +- name: Check if db3 database create command is present in the dumped file + shell: "grep -i 'CREATE DATABASE.*`{{ db3_name }}`' {{ dump1_file }}" + +# ========================================================================== +# Dump all databases + +- name: state dump - dump2 file name should not exist + file: + name: '{{ dump2_file }}' + state: absent + +- name: Dump existing databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: all + state: dump + target: '{{ dump2_file }}' + register: dump_result + +- name: assert successful completion of dump operation + assert: + that: + - dump_result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist + assert: + that: + - "'{{ db1_name }}' in mysql_result.stdout" + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + - "'{{ db5_name }}' not in mysql_result.stdout" + +- name: state dump - file name should exist (dump2_file) + file: + name: '{{ dump2_file }}' + state: file + +# ============================ DELETE TEST ================================= +# +# ========================================================================== +# Delete multiple databases which already exists (check mode) +- name: Delete multiple databases which already exists (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db2_name }}' + - '{{ db3_name }}' + state: absent + register: check_mode_result + check_mode: yes + +- name: assert successful completion of delete databases which already exists using check mode + assert: + that: + - check_mode_result is changed + +- name: run command to test state=absent for a database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases exist even after deleting (since deleted via check mode) + assert: + that: + - "'{{ db2_name }}' in mysql_result.stdout" + - "'{{ db3_name }}' in mysql_result.stdout" + +# ========================================================================== +# Delete multiple databases +- name: Delete multiple databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db2_name }}' + - '{{ db3_name }}' + state: absent + register: result + +- name: assert successful completion of deleting database + assert: + that: + - result is changed + - result.db_list == ['{{ db2_name }}', '{{ db3_name }}'] + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does not exist + assert: + that: + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' not in mysql_result.stdout" + +# ========================================================================== +# Delete non existing databases (check mode) +- name: Delete non existing databases (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db2_name }}' + - '{{ db4_name }}' + state: absent + register: check_mode_result + check_mode: yes + +- name: assert that deletion of non existing databases does not make change (using check mode) + assert: + that: + - check_mode_result is not changed + +- name: run command to test state=absent for a database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does not exist since were deleted priorly (check mode) + assert: + that: + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + +# ========================================================================== +# Delete already deleted databases +- name: Delete already deleted databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db2_name }}' + - '{{ db4_name }}' + state: absent + register: result + +- name: assert that deletion of non existing databases does not make change + assert: + that: + - result is not changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that databases does not exists + assert: + that: + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + +# ========================================================================== +# Delete all databases +- name: Delete all databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db1_name }}' + - '{{ db2_name }}' + - '{{ db3_name }}' + - '{{ db4_name }}' + - '{{ db5_name }}' + state: absent + register: result + +- name: assert successful completion of deleting database + assert: + that: + - result is changed + +- name: run command to list databases like specified database name + command: "{{ mysql_command }} \"-e show databases like 'database%'\"" + register: mysql_result + +- name: assert that specific databases does not exist + assert: + that: + - "'{{ db1_name }}' not in mysql_result.stdout" + - "'{{ db2_name }}' not in mysql_result.stdout" + - "'{{ db3_name }}' not in mysql_result.stdout" + - "'{{ db4_name }}' not in mysql_result.stdout" + - "'{{ db5_name }}' not in mysql_result.stdout" + +- name: state dump - dump 1 file name should be removed + file: + name: '{{ dump1_file }}' + state: absent + +- name: state dump - dump 2 file name should be removed + file: + name: '{{ dump2_file }}' + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml new file mode 100644 index 000000000..b4f9cda9b --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml @@ -0,0 +1,491 @@ +# test code for state dump and import for mysql_db module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +- name: Dump and Import | Set facts + set_fact: + db_file_name: "{{ tmp_dir }}/{{ file }}" + wrong_sql_file: "{{ tmp_dir }}/wrong.sql" + dump_file1: "{{ tmp_dir }}/{{ file2 }}" + dump_file2: "{{ tmp_dir }}/{{ file3 }}" + db_user: "test" + db_user_unsafe_password: "pass!word" + config_file: "{{ playbook_dir }}/root/.my.cnf" + +- name: Dump and Import | Create custom config file + shell: 'echo "[client]" > {{ config_file }}' + +- name: Dump and Import | Create user for test unsafe_login_password parameter + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user }}' + host: '%' + password: '{{ db_user_unsafe_password }}' + priv: '*.*:ALL' + state: present + +- name: Dump and Import | State dump/import - create database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + check_implicit_admin: yes + +- name: Dump and Import | Create database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: present + check_implicit_admin: no + +- name: Dump and Import | State dump/import - create table department + command: "{{ mysql_command }} {{ db_name }} \"-e create table department(id int, name varchar(100))\"" + +- name: Dump and Import | State dump/import - create table employee + command: "{{ mysql_command }} {{ db_name }} \"-e create table employee(id int, name varchar(100))\"" + +- name: Dump and Import | State dump/import - insert data into table employee + command: "{{ mysql_command }} {{ db_name }} \"-e insert into employee value(47,'Joe Smith')\"" + +- name: Dump and Import | State dump/import - insert data into table department + command: "{{ mysql_command }} {{ db_name }} \"-e insert into department value(2,'Engineering')\"" + +- name: Dump and Import | State dump/import - file name should not exist + file: + name: '{{ db_file_name }}' + state: absent + +- name: Dump and Import | Database dump file1 should not exist + file: + name: '{{ dump_file1 }}' + state: absent + +- name: Dump and Import | Database dump file2 should not exist + file: + name: '{{ dump_file2 }}' + state: absent + +- name: Dump and Import | State dump without department table. + mysql_db: + login_user: '{{ db_user }}' + login_password: '{{ db_user_unsafe_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + unsafe_login_password: yes + name: '{{ db_name }}' + state: dump + target: '{{ db_file_name }}' + ignore_tables: + - "{{ db_name }}.department" + force: yes + master_data: 1 + skip_lock_tables: yes + dump_extra_args: >- + --skip-triggers + config_file: '{{ config_file }}' + restrict_config_file: yes + check_implicit_admin: no + register: result + +- name: Dump and Import | Assert successful completion of dump operation + assert: + that: + - result is changed + - result.executed_commands[0] is search(".department --master-data=1 --skip-triggers") + +- name: Dump and Import | State dump/import - file name should exist (db_file_name) + file: + name: '{{ db_file_name }}' + state: file + +- name: Dump and Import | State dump with multiple databases in comma separated form for MySQL. + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: "{{ db_name }},{{ db_name2 }}" + state: dump + target: '{{ dump_file1 }}' + check_implicit_admin: yes + register: dump_result1 + +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in comma separated form) + assert: + that: + - dump_result1 is changed + - dump_result1.executed_commands[0] is search(" --user=root --password=\*\*\*\*\*\*\*\*") + +- name: Dump and Import | State dump - dump file1 should exist + file: + name: '{{ dump_file1 }}' + state: file + +- name: Dump and Import | State dump with multiple databases in list form via check_mode + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db_name }}' + - '{{ db_name2 }}' + state: dump + target: '{{ dump_file2 }}' + register: dump_result + check_mode: yes + +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in list form) via check mode + assert: + that: + - dump_result is changed + +- name: Dump and Import | Database dump file2 should not exist + stat: + path: '{{ dump_file2 }}' + register: stat_result + +- name: Dump and Import | Assert that check_mode does not create dump file for databases + assert: + that: + - stat_result.stat.exists is defined and not stat_result.stat.exists + +- name: Dump and Import | State dump with multiple databases in list form. + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: + - '{{ db_name }}' + - '{{ db_name2 }}' + state: dump + target: '{{ dump_file2 }}' + register: dump_result2 + +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in list form) + assert: + that: + - dump_result2 is changed + +- name: Dump and Import | State dump - dump file2 should exist + file: + name: '{{ dump_file2 }}' + state: file + +- name: Dump and Import | State dump/import - remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: absent + +- name: Dump and Import | Remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: absent + +- name: Dump and Import | Test state=import to restore the database of type {{ format_type }} (expect changed=true) + mysql_db: + login_user: '{{ db_user }}' + login_password: '{{ db_user_unsafe_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + unsafe_login_password: yes + name: '{{ db_name }}' + state: import + target: '{{ db_file_name }}' + use_shell: yes + register: result + +- name: Dump and Import | Show the tables + command: "{{ mysql_command }} {{ db_name }} \"-e show tables\"" + register: result + +- name: Dump and Import | Assert that the department table is absent. + assert: + that: + - "'department' not in result.stdout" + +- name: Dump and Import | Test state=import to restore a database from multiple database dumped file1 + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: import + target: '{{ dump_file1 }}' + use_shell: no + register: import_result + +- name: Dump and Import | Assert output message restored a database from dump file1 + assert: + that: + - import_result is changed + +- name: Dump and Import | Remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: absent + +- name: Dump and Import | Run command to list databases + command: "{{ mysql_command }} \"-e show databases like 'data%'\"" + register: mysql_result + +- name: Dump and Import | Assert that db_name2 database does not exist + assert: + that: + - "'{{ db_name2 }}' not in mysql_result.stdout" + +- name: Dump and Import | Test state=import to restore a database from dumped file2 (check mode) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: import + target: '{{ dump_file2 }}' + register: check_import_result + check_mode: yes + +- name: Dump and Import | Assert output message restored a database from dump file2 (check mode) + assert: + that: + - check_import_result is changed + +- name: Dump and Import | Run command to list databases + command: "{{ mysql_command }} \"-e show databases like 'data%'\"" + register: mysql_result + +- name: Dump and Import | Assert that db_name2 database does not exist (check mode) + assert: + that: + - "'{{ db_name2 }}' not in mysql_result.stdout" + +- name: Dump and Import | Test state=import to restore a database from multiple database dumped file2 + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name2 }}' + state: import + target: '{{ dump_file2 }}' + register: import_result2 + +- name: Dump and Import | Assert output message restored a database from dump file2 + assert: + that: + - import_result2 is changed + - import_result2.db_list == ['{{ db_name2 }}'] + +- name: Dump and Import | Run command to list databases + command: "{{ mysql_command }} \"-e show databases like 'data%'\"" + register: mysql_result + +- name: Dump and Import | Assert that db_name2 database does exist after import + assert: + that: + - "'{{ db_name2 }}' in mysql_result.stdout" + +- name: Dump and Import | Test state=dump to backup the database of type {{ format_type }} (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: dump + target: '{{ db_file_name }}' + register: result + +- name: Dump and Import | Assert output message backup the database + assert: + that: + - result is changed + - "result.db =='{{ db_name }}'" + +# - name: Dump and Import | Assert database was backed up successfully +# command: "file {{ db_file_name }}" +# register: result +# +# - name: Dump and Import | Assert file format type +# assert: +# that: +# - "'{{ format_msg_type }}' in result.stdout" + +- name: Dump and Import | Update database table employee + command: "{{ mysql_command }} {{ db_name }} \"-e update employee set name='John Doe' where id=47\"" + +- name: Dump and Import | Test state=import to restore the database of type {{ format_type }} (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '{{ db_file_name }}' + register: result + +- name: Dump and Import | Assert output message restore the database + assert: + that: + - result is changed + +- name: Dump and Import | Select data from table employee + command: "{{ mysql_command }} {{ db_name }} \"-e select * from employee\"" + register: result + +- name: Dump and Import | Assert data in database is from the restore database + assert: + that: + - "'47' in result.stdout" + - "'Joe Smith' in result.stdout" + +########################## +# Test ``force`` parameter +########################## + +- name: Dump and Import | Create wrong sql file + shell: echo 'CREATE TABLE hello (id int); CREATE ELBAT ehlo (int id);' >> '{{ wrong_sql_file }}' + +- name: Dump and Import | Try to import without force parameter, must fail + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '{{ wrong_sql_file }}' + force: no + register: result + ignore_errors: yes + +- assert: + that: + - result is failed + +- name: Dump and Import | Try to import with force parameter + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '{{ wrong_sql_file }}' + force: yes + register: result + +- assert: + that: + - result is changed + +######################## +# Test import with chdir + +- name: Dump and Import | Create dir + file: + path: ~/subdir + state: directory + +- name: Dump and Import | Create test dump + shell: 'echo "SOURCE ./subdir_test.sql" > ~/original_test.sql' + +- name: Dump and Import | Create test source + shell: 'echo "SELECT 1" > ~/subdir/subdir_test.sql' + +- name: Dump and Import | Try to restore without chdir argument, must fail + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '~/original_test.sql' + ignore_errors: yes + register: result +- assert: + that: + - result is failed + - result.msg is search('Failed to open file') + +- name: Dump and Import | Restore with chdir argument, must pass + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '~/original_test.sql' + chdir: ~/subdir + register: result +- assert: + that: + - result is succeeded + +########## +# Clean up +########## + +- name: Dump and Import | Clean up databases + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ item }}' + state: absent + loop: + - '{{ db_name }}' + - '{{ db_name2 }}' + +- name: Dump and Import | Clean up files + file: + name: '{{ item }}' + state: absent + loop: + - '{{ db_file_name }}' + - '{{ wrong_sql_file }}' + - '{{ dump_file1 }}' + - '{{ dump_file2 }}' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml new file mode 100644 index 000000000..12633f2a7 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml @@ -0,0 +1,302 @@ +--- +# test code for mysql_db module with database name containing special chars + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +- name: State Present Absent | Remove database if it exists + command: > + "{{ mysql_command }} -sse 'DROP DATABASE IF EXISTS {{ db_name }}'" + ignore_errors: true + +- name: State Present Absent | Make sure the test database is not there + command: "{{ mysql_command }} {{ db_name }}" + register: mysql_db_check + failed_when: "'1049' not in mysql_db_check.stderr" + +- name: State Present Absent | Test state=present for a database name (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + register: result + +- name: State Present Absent | Assert output message that database exist + assert: + that: + - result is changed + - result.db == '{{ db_name }}' + - result.executed_commands == ["CREATE DATABASE `{{ db_name }}`"] + +- name: State Present Absent | Run command to test state=present for a database name (expect db_name in stdout) + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database exist + assert: + that: + - "'{{ db_name }}' in result.stdout" + +# ============================================================ +- name: State Present Absent | Test state=absent for a database name (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: absent + register: result + +- name: State Present Absent | Assert output message that database does not exist + assert: + that: + - result is changed + - result.db == '{{ db_name }}' + - result.executed_commands == ["DROP DATABASE `{{ db_name }}`"] + +- name: State Present Absent | Run command to test state=absent for a database name (expect db_name not in stdout) + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database does not exist + assert: + that: + - "'{{ db_name }}' not in result.stdout" + +# ============================================================ +- name: State Present Absent | Test mysql_db encoding param not valid - issue 8075 + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: datanotvalid + state: present + encoding: notvalid + register: result + ignore_errors: true + +- name: State Present Absent | Assert test mysql_db encoding param not valid - issue 8075 (failed=true) + assert: + that: + - result is failed + - "'Traceback' not in result.msg" + - "'Unknown character set' in result.msg" + +# ============================================================ +- name: State Present Absent | Test mysql_db using a valid encoding utf8 (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: present + encoding: utf8 + register: result + +- name: State Present Absent | Assert output message created a database + assert: + that: + - result is changed + - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'utf8'"] + +- name: State Present Absent | Test database was created + command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE `en{{ db_name }}`\"" + register: result + +- name: State Present Absent | Assert created database is of encoding utf8 + assert: + that: + - "'utf8' in result.stdout" + +- name: State Present Absent | Remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: absent + +# ============================================================ +- name: State Present Absent | Test mysql_db using valid encoding binary (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: present + encoding: binary + register: result + +- name: State Present Absent | Assert output message that database was created + assert: + that: + - result is changed + - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'binary'"] + +- name: State Present Absent | Run command to test database was created + command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE `en{{ db_name }}`\"" + register: result + +- name: State Present Absent | Assert created database is of encoding binary + assert: + that: + - "'binary' in result.stdout" + +- name: State Present Absent | Remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: absent + +# ============================================================ +- name: State Present Absent | Create user1 to access database dbuser1 + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: user1 + host: '%' + password: 'Hfd6fds^dfA8Ga' + priv: '*.*:ALL' + state: present + +- name: State Present Absent | Create database dbuser1 using user1 + mysql_db: + login_user: user1 + login_password: 'Hfd6fds^dfA8Ga' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: present + register: result + +- name: State Present Absent | Assert output message that database was created + assert: + that: + - result is changed + +- name: State Present Absent | Run command to test database was created using user1 + command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database exist + assert: + that: + - "'{{ db_user1 }}' in result.stdout" + +# ============================================================ +- name: State Present Absent | Create user2 to access database with privilege select only + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: user2 + password: 'kjsfd&F7safjad' + priv: '*.*:SELECT' + state: present + +- name: State Present Absent | Create database dbuser2 using user2 with no privilege to create (expect failed=true) + mysql_db: + login_user: user2 + login_password: 'kjsfd&F7safjad' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user2 }}' + state: present + register: result + ignore_errors: true + +- name: State Present Absent | Assert output message that database was not created using dbuser2 + assert: + that: + - result is failed + - "'Access denied' in result.msg" + +- name: State Present Absent | Run command to test that database was not created + command: "{{ mysql_command }} -e \"show databases like '{{ db_user2 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database does not exist + assert: + that: + - "'{{ db_user2 }}' not in result.stdout" + +# ============================================================ +- name: State Present Absent | Delete database using user2 with no privilege to delete (expect failed=true) + mysql_db: + login_user: user2 + login_password: 'kjsfd&F7safjad' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: absent + register: result + ignore_errors: true + +- name: State Present Absent | Assert output message that database was not deleted using dbuser2 + assert: + that: + - result is failed + - "'Access denied' in result.msg" + +- name: State Present Absent | Run command to test database was not deleted + command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database still exist + assert: + that: + - "'{{ db_user1 }}' in result.stdout" + +# ============================================================ +- name: State Present Absent | Delete database using user1 with all privilege to delete a database (expect changed=true) + mysql_db: + login_user: user1 + login_password: 'Hfd6fds^dfA8Ga' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: absent + register: result + ignore_errors: true + +- name: State Present Absent | Assert output message that database was deleted using user1 + assert: + that: + - result is changed + - result.executed_commands == ["DROP DATABASE `{{ db_user1 }}`"] + +- name: State Present Absent | Run command to test database was deleted using user1 + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: State Present Absent | Assert database does not exist + assert: + that: + - "'{{ db_user1 }}' not in result.stdout" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/defaults/main.yml new file mode 100644 index 000000000..e1cd88000 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/defaults/main.yml @@ -0,0 +1,11 @@ +--- +# defaults file for test_mysql_info +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 + +db_name: data + +user_name_1: 'db_user1' +user_password_1: 'gadfFDSdtTU^Sdfuj' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/meta/main.yml new file mode 100644 index 000000000..4be5f5879 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_controller + - setup_remote_tmp_dir diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/connector_info.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/connector_info.yml new file mode 100644 index 000000000..d525e8e9e --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/connector_info.yml @@ -0,0 +1,30 @@ +--- +# Added in 3.6.0 in +# https://github.com/ansible-collections/community.mysql/pull/497 + +- name: Connector info | Assert connector_name exists and has expected values + ansible.builtin.assert: + that: + - result.connector_name is defined + - result.connector_name is in ['pymysql', 'MySQLdb'] + success_msg: >- + Assertions passed, result.connector_name is {{ result.connector_name }} + fail_msg: >- + Assertion failed, result.connector_name is + {{ result.connector_name | d('Unknown')}} which is different than expected + pymysql or MySQLdb + +- name: Connector info | Assert connector_version exists and has expected values + ansible.builtin.assert: + that: + - result.connector_version is defined + - > + result.connector_version == 'Unknown' + or result.connector_version is version(connector_version, '==') + success_msg: >- + Assertions passed, result.connector_version is + {{ result.connector_version }} + fail_msg: >- + Assertion failed, result.connector_version is + {{ result.connector_version }} which is different than expected + {{ connector_version }} diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/issue-28.yml new file mode 100644 index 000000000..83e6883f7 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/issue-28.yml @@ -0,0 +1,86 @@ +--- + +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + - name: create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: "%" + password: "{{ user_password_1 }}" + tls_requires: + SSL: + + - name: attempt connection with newly created user (expect failure) + mysql_info: + filter: version + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: attempt connection with newly created user ignoring hostname + mysql_info: + filter: version + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: no + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/main.yml new file mode 100644 index 000000000..be367f068 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/tasks/main.yml @@ -0,0 +1,221 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Test code for mysql_info module +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +################### +# Prepare for tests +# + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # Create default MySQL config file with credentials + - name: mysql_info - create default config file + template: + src: my.cnf.j2 + dest: "{{ playbook_dir }}/root/.my.cnf" + mode: '0400' + + # Create non-default MySQL config file with credentials + - name: mysql_info - create non-default config file + template: + src: my.cnf.j2 + dest: "{{ playbook_dir }}/root/non-default_my.cnf" + mode: '0400' + + ############### + # Do tests + + # Access by default cred file + - name: mysql_info - collect default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + config_file: "{{ playbook_dir }}/root/.my.cnf" + register: result + + - assert: + that: + - result is not changed + - db_version in result.version.full + - result.settings != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.users != {} + + - name: mysql_info - Test connector informations display + ansible.builtin.import_tasks: + file: connector_info.yml + + # Access by non-default cred file + - name: mysql_info - check non-default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + config_file: "{{ playbook_dir }}/root/non-default_my.cnf" + register: result + + - assert: + that: + - result is not changed + - result.version != {} + + # Remove cred files + - name: mysql_info - remove cred files + file: + path: '{{ item }}' + state: absent + loop: + - "{{ playbook_dir }}/.my.cnf" + - "{{ playbook_dir }}/non-default_my.cnf" + + # Access with password + - name: mysql_info - check access with password + mysql_info: + <<: *mysql_params + register: result + + - assert: + that: + - result is not changed + - result.version != {} + + # Test excluding + - name: Collect all info except settings and users + mysql_info: + <<: *mysql_params + filter: '!settings,!users' + register: result + + - assert: + that: + - result is not changed + - result.version != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.settings is not defined + - result.users is not defined + + # Test including + - name: Collect info only about version and databases + mysql_info: + <<: *mysql_params + filter: + - version + - databases + register: result + + - assert: + that: + - result is not changed + - result.version != {} + - result.databases != {} + - result.engines is not defined + - result.settings is not defined + - result.global_status is not defined + - result.users is not defined + + # Test exclude_fields: db_size + # 'unsupported' element is passed to check that an unsupported value + # won't break anything (will be ignored regarding to the module's documentation). + - name: Collect info about databases excluding their sizes + mysql_info: + <<: *mysql_params + filter: + - databases + exclude_fields: + - db_size + - unsupported + register: result + + - assert: + that: + - result is not changed + - result.databases != {} + - result.databases.mysql == {} + + ######################################################## + # Issue #65727, empty databases must be in returned dict + # + - name: Create empty database acme + mysql_db: + <<: *mysql_params + name: acme + + - name: Collect info about databases + mysql_info: + <<: *mysql_params + filter: + - databases + return_empty_dbs: true + register: result + + # Check acme is in returned dict + - assert: + that: + - result is not changed + - result.databases.acme.size == 0 + - result.databases.mysql != {} + + - name: Collect info about databases excluding their sizes + mysql_info: + <<: *mysql_params + filter: + - databases + exclude_fields: + - db_size + return_empty_dbs: true + register: result + + # Check acme is in returned dict + - assert: + that: + - result is not changed + - result.databases.acme == {} + - result.databases.mysql == {} + + - name: Remove acme database + mysql_db: + <<: *mysql_params + name: acme + state: absent + + - include_tasks: issue-28.yml + + # https://github.com/ansible-collections/community.mysql/issues/204 + - name: Create database containing only views + mysql_db: + <<: *mysql_params + name: allviews + + - name: Create view + mysql_query: + <<: *mysql_params + login_db: allviews + query: 'CREATE VIEW v_today (today) AS SELECT CURRENT_DATE' + + - name: Fetch info + mysql_info: + <<: *mysql_params + register: result + + - name: Check + assert: + that: + - result.databases.allviews.size == 0 diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/templates/my.cnf.j2 b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/templates/my.cnf.j2 new file mode 100644 index 000000000..7d159a212 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_info/templates/my.cnf.j2 @@ -0,0 +1,5 @@ +[client] +user={{ mysql_user }} +password={{ mysql_password }} +host={{ mysql_host }} +port={{ mysql_primary_port }} diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/defaults/main.yml new file mode 100644 index 000000000..6befdcf5d --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/defaults/main.yml @@ -0,0 +1,15 @@ +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 + +db_name: data +test_db: testdb +test_table1: test1 +test_table2: test2 +test_table3: test3 +test_table4: test4 +test_script_path: /tmp/test.sql + +user_name_1: 'db_user1' +user_password_1: 'gadfFDSdtTU^Sdfuj' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/meta/main.yml new file mode 100644 index 000000000..01ee3db79 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_controller diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/issue-28.yml new file mode 100644 index 000000000..e788feaf8 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/issue-28.yml @@ -0,0 +1,86 @@ +--- + +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + - name: create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: "%" + password: "{{ user_password_1 }}" + tls_requires: + SSL: + + - name: attempt connection with newly created user (expect failure) + mysql_query: + query: 'SHOW DATABASES' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: attempt connection with newly created user ignoring hostname + mysql_query: + query: 'SHOW DATABASES' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: no + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host: "%" + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/main.yml new file mode 100644 index 000000000..ffb54e283 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/main.yml @@ -0,0 +1,9 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# mysql_query module initial CI tests +- import_tasks: mysql_query_initial.yml + +- include_tasks: issue-28.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml new file mode 100644 index 000000000..82665afce --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml @@ -0,0 +1,405 @@ +--- +# Test code for mysql_query module +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Create db {{ test_db }} + mysql_query: + <<: *mysql_params + query: 'CREATE DATABASE {{ test_db }}' + register: result + + - name: Assert that create db test_db is changed and returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['CREATE DATABASE {{ test_db }}'] + + - name: Create {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'CREATE TABLE {{ test_table1 }} (id int)' + register: result + + - name: Assert that create table test_table1 is changed and returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)'] + + - name: Insert test data + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: + - 'INSERT INTO {{ test_table1 }} VALUES (1), (2)' + - 'INSERT INTO {{ test_table1 }} VALUES (3)' + single_transaction: yes + register: result + + - name: Assert that inserting test data is changed and returns expected query and results + assert: + that: + - result is changed + - result.rowcount == [2, 1] + - result.executed_queries == ['INSERT INTO {{ test_table1 }} VALUES (1), (2)', 'INSERT INTO {{ test_table1 }} VALUES (3)'] + + - name: Check data in {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }}' + register: result + + - name: Assert that query data in test_table1 is not changed and returns expected query and results + assert: + that: + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }}'] + - result.rowcount == [3] + - result.query_result[0][0].id == 1 + - result.query_result[0][1].id == 2 + - result.query_result[0][2].id == 3 + + - name: Check data in {{ test_table1 }} using positional args + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }} WHERE id = %s' + positional_args: + - 1 + register: result + + - name: Assert that query data in test_table1 using positional args is not changed and returns expected query and results + assert: + that: + - result is not changed + - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] + - result.rowcount == [1] + - result.query_result[0][0].id == 1 + + - name: Check data in {{ test_table1 }} using named args + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }} WHERE id = %(some_id)s' + named_args: + some_id: 1 + register: result + + - name: Assert that query data in test_table1 using named args is not changed and returns expected query and results + assert: + that: + - result is not changed + - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] + - result.rowcount == [1] + - result.query_result[0][0].id == 1 + + - name: Update data in {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'UPDATE {{ test_table1 }} SET id = %(new_id)s WHERE id = %(current_id)s' + named_args: + current_id: 1 + new_id: 0 + register: result + + - name: Assert that update data in test_table1 is changed and returns the expected query + assert: + that: + - result is changed + - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] + - result.rowcount == [1] + + - name: Check the prev update - row with value 1 does not exist anymore + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }} WHERE id = %(some_id)s' + named_args: + some_id: 1 + register: result + + - name: Assert that query that check the prev update is not changed and returns the expected query with id = 1 + assert: + that: + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 1'] + - result.rowcount == [0] + + - name: Check the prev update - row with value - exist + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }} WHERE id = %(some_id)s' + named_args: + some_id: 0 + register: result + + - name: Assert that query that check the prev update is not changed and returns the expected query with id = 0 + assert: + that: + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [1] + + - name: Update data in {{ test_table1 }} again + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'UPDATE {{ test_table1 }} SET id = %(new_id)s WHERE id = %(current_id)s' + named_args: + current_id: 1 + new_id: 0 + register: result + + - name: Assert that update data in test_table1 again is not changed and returns expected query + assert: + that: + - result is not changed + - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] + - result.rowcount == [0] + + - name: Delete data from {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: + - 'DELETE FROM {{ test_table1 }} WHERE id = 0' + - 'SELECT * FROM {{ test_table1 }} WHERE id = 0' + register: result + + - name: Assert that delete data from test_table1 is changed an returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0', 'SELECT * FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [1, 0] + + - name: Delete data from {{ test_table1 }} again + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'DELETE FROM {{ test_table1 }} WHERE id = 0' + register: result + + - name: Assert that delete data from test_table1 again is not changed and returns expected query + assert: + that: + - result is not changed + - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [0] + + - name: Truncate {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: + - 'TRUNCATE {{ test_table1 }}' + - 'SELECT * FROM {{ test_table1 }}' + register: result + + - name: Assert that truncate test_table1 is changed and returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['TRUNCATE {{ test_table1 }}', 'SELECT * FROM {{ test_table1 }}'] + - result.rowcount == [0, 0] + + - name: Rename {{ test_table1 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'RENAME TABLE {{ test_table1 }} TO {{ test_table2 }}' + register: result + + - name: Assert that rename table test_table1 is changed and returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['RENAME TABLE {{ test_table1 }} TO {{ test_table2 }}'] + - result.rowcount == [0] + + - name: Check the prev rename + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table1 }}' + register: result + ignore_errors: yes + + - name: Assert that query old table is failed + assert: + that: + - result is failed + + - name: Check the prev rename + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT * FROM {{ test_table2 }}' + register: result + + - name: Assert that query new table succeed and returns 0 row + assert: + that: + - result.rowcount == [0] + + - name: Create {{ test_table3 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'CREATE TABLE {{ test_table3 }} (id int, story text)' + + - name: Add data to {{ test_table3 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "INSERT INTO {{ test_table3 }} (id, story) VALUES (1, 'first'), (2, 'second')" + + - name: Select from {{ test_table3 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'SELECT id, story FROM {{ test_table3 }}' + register: result + + - name: Assert that select from test_table3 returns 2 rows + assert: + that: + - result.rowcount == [2] + + - name: Pass wrong query type + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: {'this type is': 'wrong'} + register: result + ignore_errors: yes + + - name: Assert that pass wrong query type is failed + assert: + that: + - result is failed + - result.msg is search('the query option value must be a string or list') + + - name: Pass wrong query element + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: + - 'SELECT now()' + - {'this type is': 'wrong'} + register: result + ignore_errors: yes + + - name: Assert that pass wrong query element is failed + assert: + that: + - result is failed + - result.msg is search('the elements in query list must be strings') + + - name: Create {{ test_table4 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'CREATE TABLE {{ test_table4 }} (id int primary key, story text)' + + - name: Insert test data using replace statement + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "REPLACE INTO {{ test_table4 }} VALUES (1, 'first')" + single_transaction: yes + register: result + + - name: Assert that insert test data using replace statement is changed + assert: + that: + - result is changed + - result.rowcount == [1] + + - name: Replace test data + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "REPLACE INTO {{ test_table4 }} VALUES (1, 'one')" + single_transaction: yes + register: result + + - assert: + that: + - result is changed + - result.rowcount == [2] + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Create table + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "CREATE TABLE issue268 (id int)" + single_transaction: yes + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Create table with IF NOT EXISTS + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "CREATE TABLE IF NOT EXISTS issue268 (id int)" + single_transaction: yes + register: result + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Assert that create table IF NOT EXISTS is not changed with pymysql + assert: + that: + # PyMySQL driver throws a warning for version before 0.10.0 + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Assert that create table IF NOT EXISTS is changed with mysqlclient + assert: + that: + # Mysqlclient 2.0.1 and pymysql 0.10.0+ drivers throws no warning, + # so it's impossible to figure out if the state was changed or not. + # We assume that it was for DDL queries by default in the code + - result is changed + when: + - > + connector_name == 'mysqlclient' + or ( + connector_name == 'pymysql' + and connector_version is version('0.10.0', '>') + ) + + - name: Drop db {{ test_db }} + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE {{ test_db }}' + register: result + + - name: Assert that drop database is changed and returns expected query + assert: + that: + - result is changed + - result.executed_queries == ['DROP DATABASE {{ test_db }}'] + + always: + + - name: Clean up test_db + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE IF EXISTS {{ test_db }}' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/defaults/main.yml new file mode 100644 index 000000000..48fd56020 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/defaults/main.yml @@ -0,0 +1,17 @@ +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 +mysql_replica1_port: 3308 +mysql_replica2_port: 3309 + +test_db: test_db +test_table: test_table +test_primary_delay: 60 +replication_user: replication_user +replication_pass: replication_pass +dump_path: /tmp/dump.sql +test_channel: test_channel-1 + +user_name_1: 'db_user1' +user_password_1: 'gadfFDSdtTU^Sdfuj' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/meta/main.yml new file mode 100644 index 000000000..01ee3db79 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_controller diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml new file mode 100644 index 000000000..1718b9982 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml @@ -0,0 +1,167 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + block: + + # start replica so it is available for testing + + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + + - name: Drop {{ user_name_1 }} if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host: '{{ gateway_addr }}' + state: absent + ignore_errors: yes + + # First test + # check if user creation works with force_context and is replicated + - name: create user with force_context + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: '{{ gateway_addr }}' + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + force_context: yes + + - name: attempt connection on replica1 with newly created user (expect success) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded + + - name: Drop user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host: '{{ gateway_addr }}' + state: absent + force_context: yes + + - name: attempt connection on replica with freshly removed user (expect failure) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + + # Prepare replica1 for testing with a replication filter in place + # Stop replication, create a filter and restart replication on replica1. + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Create replication filter MySQL + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = (mysql);\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mysql' + + - name: Create replication filter MariaDB + shell: "echo \"SET GLOBAL replicate_ignore_db = 'mysql';\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mariadb' + + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + + # Second test + # Filter in place, ready to test if user creation is filtered with force_context + - name: create user with force_context + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: "{{ gateway_addr }}" + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + force_context: yes + + - name: attempt connection on replica with newly created user (expect failure) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + + - name: Drop user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host: "{{ gateway_addr }}" + state: absent + force_context: yes + + # restore normal replica1 operation + # Stop replication and remove the filter + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Remove replication filter MySQL + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = ();\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mysql' + + - name: Remove replication filter MariaDB + shell: "echo \"SET GLOBAL replicate_ignore_db = '';\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mariadb' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml new file mode 100644 index 000000000..4225a0760 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml @@ -0,0 +1,86 @@ +--- + +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + - name: create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + tls_requires: + SSL: + + - name: attempt connection with newly created user (expect failure) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: attempt connection with newly created user ignoring hostname + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: no + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host: '{{ gateway_addr }}' + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/main.yml new file mode 100644 index 000000000..ab5b4a38d --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/main.yml @@ -0,0 +1,27 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Initial CI tests of mysql_replication module: +- import_tasks: mysql_replication_initial.yml + +# Tests of replication filters and force_context +- include_tasks: issue-265.yml + +# Tests of primary_delay parameter: +- import_tasks: mysql_replication_primary_delay.yml + +# Tests of channel parameter: +- import_tasks: mysql_replication_channel.yml + when: + - db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7 + - mysql8022_and_higher == true # FIXME: mysql 5.7 should work, but our tets fails, why? + +# Tests of resetprimary mode: +- import_tasks: mysql_replication_resetprimary_mode.yml + +- include_tasks: issue-28.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml new file mode 100644 index 000000000..f438dbf09 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml @@ -0,0 +1,128 @@ +--- +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- vars: + mysql_params: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + + block: + # Get primary log file and log pos: + - name: Get primary status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: getprimary + register: mysql_primary_status + + # Test changeprimary mode: + - name: Run replication with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: changeprimary + primary_host: '{{ mysql_host }}' + primary_port: '{{ mysql_primary_port }}' + primary_user: '{{ replication_user }}' + primary_password: '{{ replication_pass }}' + primary_log_file: '{{ mysql_primary_status.File }}' + primary_log_pos: '{{ mysql_primary_status.Position }}' + channel: '{{ test_channel }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"] + + # Test startreplica mode: + - name: Start replica with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: startreplica + channel: '{{ test_channel }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["START SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["START REPLICA FOR CHANNEL '{{ test_channel }}'"] + + # Test getreplica mode: + - name: Get standby status with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: getreplica + channel: '{{ test_channel }}' + register: replica_status + + - assert: + that: + - replica_status.Is_Replica == true + - replica_status.Master_Host == '{{ mysql_host }}' + - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position + - replica_status.Master_Port == {{ mysql_primary_port }} + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status.Channel_Name == '{{ test_channel }}' + - replica_status is not changed + when: mysql8022_and_higher == false + + - assert: + that: + - replica_status.Is_Replica == true + - replica_status.Source_Host == '{{ mysql_host }}' + - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position + - replica_status.Source_Port == {{ mysql_primary_port }} + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status.Channel_Name == '{{ test_channel }}' + - replica_status is not changed + when: mysql8022_and_higher == true + + + # Test stopreplica mode: + - name: Stop replica with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: stopreplica + channel: '{{ test_channel }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"] + + # Test reset + - name: Reset replica with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: resetreplica + channel: '{{ test_channel }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"] + + # Test reset all + - name: Reset replica all with channel + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica2_port }}' + mode: resetreplicaall + channel: '{{ test_channel }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"] diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml new file mode 100644 index 000000000..ca7301c5b --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml @@ -0,0 +1,310 @@ +--- +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- vars: + mysql_params: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + + block: + - name: Set mysql8022_and_higher + set_fact: + mysql8022_and_higher: false + + - name: Set mysql8022_and_higher + set_fact: + mysql8022_and_higher: true + when: + - db_engine == 'mysql' + - db_version is version('8.0.22', '>=') + + # We use iF NOT EXISTS because the GITHUB Action: + # "ansible-community/ansible-test-gh-action" uses "--retry-on-error". + # If test_mysql_replication fails, test will run again an without the IF + # NOT EXISTS, we see "Error 1396 (HY000): Operation CREATE USER failed..." + # which is misleading. + - name: Create user for mysql replication + shell: + "echo \"CREATE USER IF NOT EXISTS \ + '{{ replication_user }}'@'{{ mysql_host }}' \ + IDENTIFIED WITH mysql_native_password BY '{{ replication_pass }}'; \ + GRANT REPLICATION SLAVE ON *.* TO \ + '{{ replication_user }}'@'{{ mysql_host }}';\" | {{ mysql_command }}" + when: db_engine == 'mysql' + + - name: Create user for mariadb replication + shell: + "echo \"CREATE USER IF NOT EXISTS \ + '{{ replication_user }}'@'{{ mysql_host }}' \ + IDENTIFIED BY '{{ replication_pass }}'; \ + GRANT REPLICATION SLAVE ON *.* TO \ + '{{ replication_user }}'@'{{ mysql_host }}';\" | {{ mysql_command }}" + when: db_engine == 'mariadb' + + - name: Create test database + mysql_db: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + state: present + name: '{{ test_db }}' + + - name: Dump all databases from the primary + shell: + cmd: >- + mysqldump + -u{{ mysql_user }} + -p{{ mysql_password }} + -h{{ mysql_host }} + -P{{ mysql_primary_port }} + --protocol=tcp + --all-databases + --ignore-table=mysql.innodb_index_stats + --ignore-table=mysql.innodb_table_stats + --master-data=2 + > {{ dump_path }} + + - name: Restore the dump to replica1 + shell: + cmd: >- + {{ mysql_command_wo_port }} + -P{{ mysql_replica1_port }} < {{ dump_path }} + + - name: Restore the dump to replica2 + shell: + cmd: >- + {{ mysql_command_wo_port }} + -P{{ mysql_replica2_port }} < {{ dump_path }} + + # Test getprimary mode: + - name: Get primary status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: getprimary + register: mysql_primary_status + + - name: Assert that primary is in expected state + assert: + that: + - mysql_primary_status.Is_Primary == true + - mysql_primary_status.Position != 0 + - mysql_primary_status is not changed + + # Test startreplica fails without changeprimary first. This needs fail_on_error + - name: Start replica and fail because primary is not specified; failing on error as requested + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + primary_use_gtid: replica_pos + fail_on_error: yes + register: result + ignore_errors: yes + + - name: Assert that startreplica is failed + assert: + that: + - result is failed + + # Test startreplica doesn't fail if fail_on_error: no + - name: Start replica and fail without propagating it to ansible as we were asked not to + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + fail_on_error: no + register: result + + - name: Assert that startreplica succeeded + assert: + that: + - result is not failed + + # Test startreplica doesn't fail if there is no fail_on_error. + # This is suboptimal because nothing happens, but it's the old behavior. + - name: Start replica and fail without propagating it to ansible as previous versions did not fail on error + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - name: Assert that start replica succeeded again + assert: + that: + - result is not failed + + # Test changeprimary mode: + # primary_ssl_ca will be set as '' to check the module's behaviour for #23976, + # must be converted to an empty string + - name: Run replication + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: changeprimary + primary_host: '{{ mysql_host }}' + primary_port: '{{ mysql_primary_port }}' + primary_user: '{{ replication_user }}' + primary_password: '{{ replication_pass }}' + primary_log_file: '{{ mysql_primary_status.File }}' + primary_log_pos: '{{ mysql_primary_status.Position }}' + primary_ssl_ca: '' + primary_ssl: no + register: result + + - name: Assert that changeprimmary is changed and return expected query + assert: + that: + - result is changed + - result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }},MASTER_SSL=0,MASTER_SSL_CA=''"] + + # Test startreplica mode: + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - name: Assert that startreplica is changed and returns expected query + assert: + that: + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + + # Test getreplica mode: + - name: Get replica status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: getreplica + register: replica_status + + - name: Assert that getreplica returns expected values for MySQL older than 8.0.22 and Mariadb + assert: + that: + - replica_status.Is_Replica == true + - replica_status.Master_Host == '{{ mysql_host }}' + - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position + - replica_status.Master_Port == {{ mysql_primary_port }} + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status is not changed + when: mysql8022_and_higher == false + + - name: Assert that getreplica returns expected values for MySQL newer than 8.0.22 + assert: + that: + - replica_status.Is_Replica == true + - replica_status.Source_Host == '{{ mysql_host }}' + - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position + - replica_status.Source_Port == {{ mysql_primary_port }} + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status is not changed + when: mysql8022_and_higher == true + + # Create test table and add data to it: + - name: Create test table + shell: "echo \"CREATE TABLE {{ test_table }} (id int);\" | {{ mysql_command_wo_port }} -P{{ mysql_primary_port }} {{ test_db }}" + + - name: Insert data + shell: "echo \"INSERT INTO {{ test_table }} (id) VALUES (1), (2), (3); FLUSH LOGS;\" | {{ mysql_command_wo_port }} -P{{ mysql_primary_port }} {{ test_db }}" + + - name: Small pause to be sure the bin log, which was flushed previously, reached the replica + ansible.builtin.wait_for: + timeout: 2 + + # Test primary log pos has been changed: + - name: Get replica status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: getreplica + register: replica_status + + # mysql_primary_status.Position is not actual and it has been changed by the prev step, + # so replica_status.Exec_Master_Log_Pos must be different: + - name: Assert that getreplica Log_Pos is different for MySQL older than 8.0.22 and MariaDB + assert: + that: + - replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position + when: mysql8022_and_higher == false + + - name: Assert that getreplica Log_Pos is different for MySQL newer than 8.0.22 + assert: + that: + - replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position + when: mysql8022_and_higher == true + + - name: Start replica that is already running + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + fail_on_error: true + register: result + + # mysqlclient 2.0.1 and pymysql 0.10.0+ always return "changed" + - name: Assert that startreplica is not changed + assert: + that: + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') + + # Test stopreplica mode: + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + register: result + + - name: Assert that stopreplica is changed and returns expected query + assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Pause for 2 seconds to let the replication stop + ansible.builtin.wait_for: + timeout: 2 + + # Test stopreplica mode: + # mysqlclient 2.0.1 and pymysql 0.10.0+ always return "changed" + - name: Stop replica that is no longer running + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + fail_on_error: true + register: result + + - name: Assert that stopreplica is not changed + assert: + that: + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') + + # master / slave related choices were removed in 3.0.0 + # https://github.com/ansible-collections/community.mysql/pull/252 + - name: Test invoking the module with unsupported choice + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopslave + fail_on_error: true + register: result + ignore_errors: yes + + - name: Assert that stopslave returns expected error message + assert: + that: + - result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, got{{ ":" }} stopslave" + - result is failed diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml new file mode 100644 index 000000000..5e967e82b --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml @@ -0,0 +1,45 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- vars: + mysql_params: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + + block: + + # Test primary_delay mode: + - name: Run replication + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: changeprimary + primary_delay: '{{ test_primary_delay }}' + register: result + + - assert: + that: + - result is changed + - result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"] + + # Auxiliary step: + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + # Check primary_delay: + - name: Get standby status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: getreplica + register: replica_status + + - assert: + that: + - replica_status.SQL_Delay == {{ test_primary_delay }} + - replica_status is not changed diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml new file mode 100644 index 000000000..4bccc76b6 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml @@ -0,0 +1,56 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- vars: + mysql_params: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + + block: + + # Needs for further tests: + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + + - name: Reset replica all + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: resetreplicaall + + # Get primary initial status: + - name: Get primary status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: getprimary + register: mysql_primary_initial_status + + # Test resetprimary mode: + - name: Reset primary + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: resetprimary + register: result + + - assert: + that: + - result is changed + - result.queries == ["RESET MASTER"] + + # Get primary final status: + - name: Get primary status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: getprimary + register: mysql_primary_final_status + + - assert: + that: + - mysql_primary_initial_status.File != mysql_primary_final_status.File diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/defaults/main.yml new file mode 100644 index 000000000..62dc5f1f3 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/defaults/main.yml @@ -0,0 +1,5 @@ +--- +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/meta/main.yml new file mode 100644 index 000000000..01ee3db79 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_controller diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/main.yml new file mode 100644 index 000000000..b517fc053 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/main.yml @@ -0,0 +1,20 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# mysql_role module initial CI tests +# TODO, many tests fails with MariaDB, debug them then remove the +# when clause and swap include_tasks for import_tasks. +- include_tasks: mysql_role_initial.yml + when: + - db_engine == 'mysql' + +# Test that subtract_privs will only revoke the grants given by priv +# (https://github.com/ansible-collections/community.mysql/issues/331) +- include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: no +- include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: yes diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml new file mode 100644 index 000000000..3762df967 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml @@ -0,0 +1,1660 @@ +--- +# Test code for mysql_role module + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Get server version + mysql_info: + <<: *mysql_params + register: srv + + - name: When run with unsupported server versions, must fail + mysql_role: + <<: *mysql_params + name: test + register: result + ignore_errors: yes + + - name: Must fail when meet unsupported version + assert: + that: + - result is failed + - result is search('Roles are not supported by the server') + when: + - srv['version']['major'] < 8 + + - name: Skip unsupported versions + meta: end_play + when: srv['version']['major'] < 8 + + ######### + # Prepare + - name: Create db test_db + mysql_db: + <<: *mysql_params + name: 'test_db' + register: result + + - name: Create table test_table + mysql_query: + <<: *mysql_params + login_db: 'test_db' + query: 'DROP TABLE IF EXISTS test_table' + register: result + + - name: Create table test_table + mysql_query: + <<: *mysql_params + login_db: 'test_db' + query: 'CREATE TABLE IF NOT EXISTS test_table (id int)' + register: result + + - name: Create users + mysql_user: + <<: *mysql_params + name: '{{ item }}' + host: '%' + password: '{{ mysql_password }}' + loop: + - 'user0' + - 'user1' + - 'user2' + + ########### + # Run tests + + - name: Create role0 in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + register: result + check_mode: yes + + - name: Assert that create role0 is changed + assert: + that: + - result is changed + + - name: Check in DB + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that user is not in mysql.user + assert: + that: + - result.rowcount.0 == 0 + + # It must fail because of check_mode + - name: Check in DB, if not granted, the query will fail (expect failure) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that show grants is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Check in DB (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that user is not in mysql.roles_mapping (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + # ===================== + + - name: Check that the user have no active roles + mysql_query: + login_user: 'user0' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + query: 'SELECT COALESCE(current_role(), "NONE") as "current_role()"' + register: result + + - name: Assert that the user have no active roles + assert: + that: + - result.query_result.0.0["current_role()"] == "NONE" + + - name: Create role role0 + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + register: result + + - name: Assert that create role is changed + assert: + that: + - result is changed + + - name: Check in DB + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 is in mysql.user + assert: + that: + - result.rowcount.0 == 1 + + - name: Query role0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that show grants is succeeded (mysql) + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Check in DB (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that role is in mysql.roles_mapping (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Check that the role is active + mysql_query: + login_user: 'user0' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + query: 'SELECT current_role()' + register: result + when: db_engine == 'mysql' + + - name: Assert that current_role() returns role0 + assert: + that: + - "'role0' in result.query_result.0.0['current_role()']" + when: db_engine == 'mysql' + + - name: Check that the role is active (mariadb) + mysql_query: + login_user: 'user0' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + query: + - 'SET ROLE role0' + - 'SELECT current_role()' + register: result + when: db_engine == 'mariadb' + + - name: Assert that role is active (mariadb) + assert: + that: + - "'role0' in result.query_result.1.0['current_role()']" + when: db_engine == 'mariadb' + + # ======================== + + - name: Create role role0 again in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + register: result + check_mode: yes + + - name: Assert that create role role0 again is not changed + assert: + that: + - result is not changed + + - name: Check in DB + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role role0 is present in the database + assert: + that: + - result.rowcount.0 == 1 + + - name: Query role0, if not granted, the query will fail (2) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query for the role0 is succeeded for mysql (2) + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Check in DB (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query for the role0 is succeeded for mariadb + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # ======================== + + - name: Create role0 again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + register: result + + - name: Assert that create role0 again is not changed + assert: + that: + - result is not changed + + - name: Query role0 + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 is in DB + assert: + that: + - result.rowcount.0 == 1 + + # ======================== + + - name: Drop role0 in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: absent + register: result + check_mode: yes + + - name: Assert that drop role0 in check_mode is changed + assert: + that: + - result is changed + + - name: Query role0 + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 is in DB + assert: + that: + - result.rowcount.0 == 1 + + # Must pass because of check_mode + - name: Query role0, if not granted, the query will fail (3) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that role0 is still in mysql after drop in check_mode (3) + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + # Must pass because of check_mode + - name: Query count for user0 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that role0 is still in mariadb after drop in check_mode + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # ======================== + + - name: Drop role0 + mysql_role: + <<: *mysql_params + name: 'role0' + state: absent + register: result + + - name: Assert that drop role0 is changed + assert: + that: + - result is changed + + - name: Query role0 + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 is absent from db + assert: + that: + - result.rowcount.0 == 0 + + - name: Query grants for role0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query for role0 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query count for user0 and role0 in mariadb + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mariadb' + + - name: Assert that query count for user0 and role0 in mariadb returns 0 rows + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + # ======================== + + - name: Drop role0 again in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: absent + register: result + check_mode: yes + + - name: Assert that drop role0 again in check_mode is not changed + assert: + that: + - result is not changed + + - name: Drop role0 again + mysql_role: + <<: *mysql_params + name: 'role0' + state: absent + register: result + + - name: Assert that drop role0 again is not changed + assert: + that: + - result is not changed + + # ================== + + - name: Create role0 in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + register: result + check_mode: yes + + - name: Assert that create role0 in check_mode is changed + assert: + that: + - result is changed + + - name: Query role0 + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 created in check_mode is not in the database + assert: + that: + - result.rowcount.0 == 0 + + # ======================== + + - name: Create role0 + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + register: result + + - name: Assert that create role0 is changed + assert: + that: + - result is changed + + - name: Query role0 + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result + + - name: Assert that role0 is in the database + assert: + that: + - result.rowcount.0 == 1 + + # ======================== + + - name: Create role0 in check_mode again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + register: result + check_mode: yes + + # TODO fix this with mariadb. I disable this test because I'm not an + # expert with roles and I don't know if it's a correct behavior of our module + # against MariaDB or if it is a bug. We never tested MariaDB properly... + - name: Assert that create role0 in check_mode again is not changed + assert: + that: + - result is not changed + when: + - db_engine == 'mysql' + + # ======================== + + - name: Create role0 again (2) + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user0@%' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + register: result + + # TODO fix this with mariadb. I disable this test because I'm not an + # expert with roles and I don't know if it's a correct behavior of our module + # against MariaDB or if it is a bug. We never tested MariaDB properly... + - name: Assert that create role0 again is not changed (2) + assert: + that: + - result is not changed + when: + - db_engine == 'mysql' + + + # ############################################## + # Test rewriting / appending / detaching members + # ############################################## + + - name: Create role1 + mysql_role: + <<: *mysql_params + name: 'role1' + state: present + register: result + + # Rewriting members + - name: Rewrite members in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user1@%' + - 'user2@%' + - 'role1' + register: result + check_mode: yes + + - name: Assert that rewrite members in check_mode is changed + assert: + that: + - result is changed + + # user0 is still a member because of check_mode + - name: Query user0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that show grants for user0 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + # user0 is still a member because of check_mode + - name: Query user0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that show grants for user0 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # user1, user2, and role1 are not members because of check_mode + - name: Query user1, if not granted, the query will fail (expect failue) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + ignore_errors: yes + register: result + when: db_engine == 'mysql' + + - name: Assert that query for user1 in mysql is failed due to check_mode + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query user1 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query for user1 in mariadb is failed due to check_mode + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + - name: Query user2, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query for user2 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query user2 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query user2 in mariadb returns 0 row + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + - name: Query role1, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role1 USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query role1 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query role1 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'role1' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query role0 in mariadb returns 0 row + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + # ======================== + + - name: Rewrite members + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user1@%' + - 'user2@%' + - 'role1' + register: result + + - name: Assert that rewrite members is changed + assert: + that: + - result is changed + + # user0 is not a member any more + - name: Query user0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query user0 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + # user0 is not a member any more + - name: Query user0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query user0 in mariadb doesn't returns role0 + assert: + that: + - "'role0' not in result.query_result.0.0['Grants for user0@%']" + when: db_engine == 'mariadb' + + - name: Query user1, if not granted, the query will fail (expect success) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user1 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query user1 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query user1 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query user2, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user2 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query user2 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query user2 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query role0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role1 USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query role0 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count user is role1 and role is role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'role1' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count user is role1 and role is role0 returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + + # ========================== + + - name: Rewrite members again in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user1@%' + - 'user2@%' + - 'role1' + register: result + check_mode: yes + + - name: Assert that rewrite members again in check_mode is not changed + assert: + that: + - result is not changed + + # ========================== + + - name: Rewrite members again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'user1@%' + - 'user2@%' + - 'role1' + register: result + + - name: Assert that rewrite members again is not changed + assert: + that: + - result is not changed + + # ========================== + + # Append members + - name: Append a member in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + append_members: yes + members: + - 'user0@%' + register: result + check_mode: yes + + - name: Assert that append a member in check_mode is changed + assert: + that: + - result is changed + + - name: Query user0, if not granted, the query will fail (expect failure) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + ignore_errors: yes + register: result + when: db_engine == 'mysql' + + - name: Assert that query user0 is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query count for user0 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user0 and role0 in mariadb resturns 0 row + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + # ===================== + + - name: Append a member + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + append_members: yes + members: + - 'user0@%' + register: result + + - name: Assert that append a member is changed + assert: + that: + - result is changed + + - name: Query user0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user0 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user0 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user0 and role0 in mariadb resturns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # user1 and user2 must still be in DB because we are appending + - name: Query user1 using role0 (expect success) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query for user1 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user1 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user1 and role0 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query user2, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user2 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user2 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user2 and role0 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # ======================== + + - name: Append a member again in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + append_members: yes + members: + - 'user0@%' + register: result + check_mode: yes + + - name: Assert that append a member again in check_mode is not changed + assert: + that: + - result is not changed + + # ======================== + + - name: Append a member again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + append_members: yes + members: + - 'user0@%' + register: result + + - name: Assert that append a member again is not changed + assert: + that: + - result is not changed + + ############## + # Detach users + - name: Detach users in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + detach_members: yes + members: + - 'user1@%' + - 'user2@%' + register: result + check_mode: yes + + - name: Assert that detach users in check_mode is changed + assert: + that: + - result is changed + + # They must be there because of check_mode + - name: Query user0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user0 is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user0 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user0 and role0 in mariadb resturns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query user1 using role0 (expect success) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user1 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user1 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user1 and role0 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query user2, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user2 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count user2 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count user2 and role0 in mariadb returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + # ======================== + + - name: Detach users + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + detach_members: yes + members: + - 'user1@%' + - 'user2@%' + register: result + + - name: Assert that detach users is changed + assert: + that: + - result is changed + + - name: Query user0, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' + + - name: Assert that query user0 in mysql is succeeded + assert: + that: + - result is succeeded + when: db_engine == 'mysql' + + - name: Query count for user0 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user0 and role0 returns 1 row + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' + + - name: Query user1, if not granted, the query will fail (expect failure) + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + ignore_errors: yes + register: result + when: db_engine == 'mysql' + + - name: Assert that query user1 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query count for user1 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user1 and role0 in mariadb returns 0 row + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + - name: Query user2, if not granted, the query will fail + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + ignore_errors: yes + when: db_engine == 'mysql' + + - name: Assert that query user2 in mysql is failed + assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Query count for user2 and role0 (mariadb) + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query count for user2 and role0 returns 0 row + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + + # ===================== + + - name: Detach users in check_mode again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + detach_members: yes + members: + - 'user1@%' + - 'user2@%' + register: result + check_mode: yes + + - name: Assert that detach users in check_mode again is not changed + assert: + that: + - result is not changed + + - name: Detach users again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + detach_members: yes + members: + - 'user1@%' + - 'user2@%' + register: result + + - name: Assert that detach users again is not changed + assert: + that: + - result is not changed + + - name: '"detach" users when creating a new role' + mysql_role: + <<: *mysql_params + name: 'role3' + state: present + detach_members: yes + members: + - 'user1@%' + register: result + + - name: Assert that creating a role while detach users is changed + assert: + that: + - result is changed + + - name: Query grants for user1 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%'" + register: result + + - name: Assert detach_members did not add a user to the role + assert: + that: + - "'role3' not in result.query_result.0.0" + + # test members_must_exist + - name: Try failing on not-existing user in check-mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: yes + append_members: yes + members: + - 'not_existent@%' + register: result + ignore_errors: yes + check_mode: yes + + - name: Assert nonexistent user in check-mode is failed + assert: + that: + - result is failed + + - name: Try failing on not-existing user in check-mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: no + append_members: yes + members: + - 'not_existent@%' + register: result + check_mode: yes + + - name: Check for lack of change + assert: + that: + - result is not changed + + - name: Try failing on not-existing user + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: yes + append_members: yes + members: + - 'not_existent@%' + register: result + ignore_errors: yes + + - name: Assert nonexistent user with members_must_exist is failed + assert: + that: + - result is failed + + - name: Try failing on not-existing user + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: no + append_members: yes + members: + - 'not_existent@%' + register: result + + - name: Assert nonexistent user with members_must_exist=no is not changed + assert: + that: + - result is not changed + + # ########## + # Test privs + # ########## + + - name: Create test DBs + mysql_query: + <<: *mysql_params + query: 'CREATE DATABASE {{ item }}' + loop: + - 'test_db1' + - 'test_db2' + register: result + + - name: Create table test_table + mysql_query: + <<: *mysql_params + login_db: '{{ item }}' + query: 'CREATE TABLE test_table (id int)' + loop: + - 'test_db1' + - 'test_db2' + register: result + + - name: Query grants for role0 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role0" + register: result + + - name: Assert grants for role0 in mysql + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.rowcount.0 == 2 + when: db_engine == 'mysql' + + - name: Assert grants for role0 in mariadb + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.rowcount.0 == 2 + when: db_engine == 'mariadb' + + - name: Append privs in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' + append_privs: yes + register: result + check_mode: yes + + - name: Assert append privs in check_mode is changed + assert: + that: + - result is changed + + - name: Query grants for role0 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role0" + register: result + + - name: Assert grants for role0 in mysql + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.rowcount.0 == 2 + when: db_engine == 'mysql' + + - name: Assert grants for role0 in mariadb + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.rowcount.0 == 2 + when: db_engine == 'mariadb' + + - name: Append privs + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' + append_privs: yes + register: result + + - name: Assert that append privs is changed + assert: + that: + - result is changed + + - name: Query grants for role0 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role0" + register: result + + - name: Assert grants for role0 in mysql + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.query_result.0.2["Grants for role0@%"] == "GRANT SELECT, INSERT ON `test_db1`.`test_table` TO `role0`@`%`" + - result.query_result.0.3["Grants for role0@%"] == "GRANT DELETE ON `test_db2`.`test_table` TO `role0`@`%`" + - result.rowcount.0 == 4 + when: db_engine == 'mysql' + + - name: Assert grants for role0 in mariadb + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.query_result.0.2["Grants for role0"] == "GRANT SELECT, INSERT ON `test_db1`.`test_table` TO `role0`" + - result.query_result.0.3["Grants for role0"] == "GRANT DELETE ON `test_db2`.`test_table` TO `role0`" + - result.rowcount.0 == 4 + when: db_engine == 'mariadb' + + - name: Append privs again in check_mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' + append_privs: yes + register: result + check_mode: yes + + # TODO it must be changed. The module uses user_mod function + # taken from mysql_user module. It's a bug / expected behavior + # because I added a similar tasks to mysql_user tests + # https://github.com/ansible-collections/community.mysql/issues/50#issuecomment-871216825 + # and it's also failed. Create an issue after the module is merged to avoid conflicts. + # TODO Fix this after user_mod is fixed. + - name: Assert that append privs again in check_mode is changed + assert: + that: + - result is changed + + - name: Append privs again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' + append_privs: yes + register: result + + - name: Assert that append privs again is not changed + assert: + that: + - result is not changed + + - name: Rewrite privs + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + priv: + '*.*': 'SELECT' + register: result + + - name: Assert that rewrite privs is changed + assert: + that: + - result is changed + + - name: Query grants for role0 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR role0" + register: result + + - name: Assert grants for role0 in mysql + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT ON *.* TO `role0`@`%`" + - result.rowcount.0 == 1 + when: db_engine == 'mysql' + + - name: Assert grants for role0 in mariadb + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT ON *.* TO `role0`" + - result.rowcount.0 == 1 + when: db_engine == 'mariadb' + + # ################# + # Test admin option + # ################# + + - name: Drop role0 + mysql_role: + <<: *mysql_params + name: 'role0' + state: absent + register: result + + - name: Create role0 with admin + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + admin: 'user0@%' + register: result + ignore_errors: yes + + - name: Assert expected error message for mysql + assert: + that: + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: db_engine == 'mysql' + + - name: Assert create role0 in mariadb is changed + assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Query role0 in mariadb + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = 'role0' AND Host = ''" + register: result + when: db_engine == 'mariadb' + + - name: Assert that query role0 in mariadb returns 1 row + assert: + that: + - result.rowcount.0 == 1 + when: db_engine == 'mariadb' + + - name: Create role0 with admin again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + admin: 'user0@%' + register: result + ignore_errors: yes + + - name: Assert expected error message in mysql again + assert: + that: + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: db_engine == 'mysql' + + - name: Assert create role0 in mariadb is not changed + assert: + that: + - result is not changed + when: db_engine == 'mariadb' + + # Try to grant a role to a user who does not exist + - name: Create role0 with admin again + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members: + - 'nonexistent@%' + register: result + ignore_errors: yes + + - name: Assert that create role0 with admin again is failed + assert: + that: + - result is failed + - result.msg is search('does not exist') + + always: + + - name: Clean up DBs + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE IF EXISTS {{ item }}' + loop: + - 'test_db' + - 'test_db1' + - 'test_db2' + + - name: Clean up users + mysql_user: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - 'user0' + - 'user1' + - 'user2' + + - name: Clean up roles + mysql_role: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - 'role0' + - 'test' + - 'role3' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml new file mode 100644 index 000000000..b79a1cb78 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml @@ -0,0 +1,164 @@ +# Test code to ensure that subtracting privileges will not result in unnecessary changes. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Create test databases + mysql_db: + <<: *mysql_params + name: data1 + state: present + + - name: Create a role with an initial set of privileges + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:SELECT,INSERT' + state: present + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the initial set of privileges matches what is expected + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Subtract privileges that are not in the current privileges, which should be a no-op + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there wasn't a change in permissions + assert: + that: + - result is not changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions still match what was originally granted + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Subtract existing and not-existing privileges, but not all + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:INSERT,DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there was a change because permissions were/would be revoked on data1.* + assert: + that: + - result is changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions were not changed if check_mode is set to 'yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that only DELETE was revoked if check_mode is set to 'no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Try to subtract invalid privileges + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:INVALID' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there was no change because invalid permissions are ignored + assert: + that: + - result is not changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions were not changed with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that the permissions were not changed with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: trigger failure by trying to subtract and append privileges at the same time + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:SELECT' + subtract_privs: yes + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + ignore_errors: true + + - name: Assert the previous execution failed + assert: + that: + - result is failed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions stayed the same, with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that the permissions stayed the same, with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + ########## + # Clean up + - name: Drop test databases + mysql_db: + <<: *mysql_params + name: 'data1' + state: present + + - name: Drop test role + mysql_role: + <<: *mysql_params + name: 'role2' + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/defaults/main.yml new file mode 100644 index 000000000..a87914cbe --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/defaults/main.yml @@ -0,0 +1,26 @@ +--- +# defaults file for test_mysql_user +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 + +db_name: 'data' +user_name_1: 'db_user1' +user_name_2: 'db_user2' +user_name_3: 'db_user3' +user_name_4: 'db_user4' + +user_password_1: 'gadfFDSdtTU^Sdfuj' +user_password_2: 'jkFKUdfhdso78yi&td' +user_password_3: 'jkFKUdfhdso78yi&tk' +user_password_4: 's2R#7pLV31!ZJrXPa3' + +root_password: 'zevuR6oPh7' + +db_names: + - clientdb + - employeedb + - providerdb + +tmp_dir: '/tmp' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-function.sql b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-function.sql new file mode 100644 index 000000000..d16118cd2 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-function.sql @@ -0,0 +1,8 @@ +USE foo; +DELIMITER ;; +CREATE FUNCTION `function` () RETURNS tinyint(4) DETERMINISTIC +BEGIN + DECLARE NAME_FOUND tinyint DEFAULT 0; + RETURN NAME_FOUND; +END;; +DELIMITER ; diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-procedure.sql b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-procedure.sql new file mode 100644 index 000000000..d0d45aa4c --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/files/create-procedure.sql @@ -0,0 +1,5 @@ +USE bar; +DELIMITER ;; +CREATE PROCEDURE `procedure` () +SELECT * FROM bar;; +DELIMITER ; diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/meta/main.yml new file mode 100644 index 000000000..4be5f5879 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_controller + - setup_remote_tmp_dir diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-121.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-121.yml new file mode 100644 index 000000000..7f5934fc6 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-121.yml @@ -0,0 +1,74 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Issue-121 | Setup | Get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect {{ mysql_host }}:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Issue-121 | Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ item }}' + host_all: true + state: absent + ignore_errors: true + loop: + - "{{ user_name_1 }}" + - "{{ user_name_2 }}" + + - name: Issue-121 | Create user with REQUIRESSL privilege (expect failure) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + priv: '*.*:SELECT,CREATE USER,REQUIRESSL,GRANT' + register: result + ignore_errors: true + + - name: Issue-121 | Assert error granting privileges + assert: + that: + - result is failed + - result.msg is search('Error granting privileges') + + - name: >- + Issue-121 | Create user with both REQUIRESSL privilege and an incompatible + tls_requires option + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: '{{ gateway_addr }}' + password: "{{ user_password_1 }}" + priv: '*.*:SELECT,CREATE USER,REQUIRESSL,GRANT' + tls_requires: + X509: + register: result + ignore_errors: true + + - name: >- + Issue-121 | Assert error granting privileges with incompatible tls_requires + option + assert: + that: + - result is failed + - result.msg is search('Error granting privileges') + + - name: Issue-121 | Teardown | Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ item }}' + host_all: true + state: absent + with_items: + - "{{ user_name_1 }}" + - "{{ user_name_2 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-265.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-265.yml new file mode 100644 index 000000000..2d8db7724 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-265.yml @@ -0,0 +1,181 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + - name: Issue-265 | Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + # Tests with force_context: yes + # Test user creation + - name: Issue-265 | Create mysql user {{ user_name_1 }} + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: present + force_context: yes + register: result + + - name: Issue-265 | Assert user was created + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost + + # Test user removal + - name: Issue-265 | remove mysql user {{ user_name_1 }} + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host_all: true + password: "{{ user_password_1 }}" + state: absent + force_context: yes + register: result + + - name: Issue-265 | Assert user was removed + assert: + that: + - result is changed + + # Test blank user removal + - name: Issue-265 | Create blank mysql user to be removed later + mysql_user: + <<: *mysql_params + name: "" + state: present + force_context: yes + password: 'KJFDY&D*Sfuydsgf' + + - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + force_context: yes + register: result + + - name: Issue-265 | Assert changed is true for removing all blank users + assert: + that: + - result is changed + + - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + force_context: yes + state: absent + register: result + + - name: Issue-265 | Assert changed is true for removing all blank users + assert: + that: + - result is not changed + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" + + # Tests with force_context: no + # Test user creation + - name: Issue-265 | Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host_all: true + state: absent + ignore_errors: yes + + # Tests with force_context: yes + # Test user creation + - name: Issue-265 | Create mysql user {{user_name_1}} + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: present + force_context: yes + register: result + + - name: Issue-265 | Assert output message mysql user was created + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost + + # Test user removal + - name: Issue-265 | Remove mysql user {{ user_name_1 }} + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: absent + force_context: no + register: result + + - name: Issue-265 | Assert output message mysql user was removed + assert: + that: + - result is changed + + # Test blank user removal + - name: Issue-265 | Create blank mysql user to be removed later + mysql_user: + <<: *mysql_params + name: "" + state: present + force_context: no + password: 'KJFDY&D*Sfuydsgf' + + - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + force_context: no + register: result + + - name: Issue-265 | Assert changed is true for removing all blank users + assert: + that: + - result is changed + + - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + force_context: no + state: absent + register: result + + - name: Issue-265 | Assert changed is true for removing all blank users + assert: + that: + - result is not changed + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_1 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-28.yml new file mode 100644 index 000000000..51a20918b --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-28.yml @@ -0,0 +1,96 @@ +--- +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: Issue-28 | Setup | Get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect {{ mysql_host }}:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Issue-28 | Setup | Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: true + + - name: Issue-28 | Create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: '{{ gateway_addr }}' + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + tls_requires: + SSL: + + - name: Issue-28 | Attempt connection with newly created user (expect failure) + mysql_user: + name: "{{ user_name_2 }}" + password: "{{ user_password_2 }}" + host: '{{ gateway_addr }}' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: true + + - name: Issue-28 | Assert connection failed + assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - name: Issue-28 | Assert connection succeeded + assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: Issue-28 | Attempt connection with newly created user ignoring hostname + mysql_user: + name: "{{ user_name_2 }}" + password: "{{ user_password_2 }}" + host: '{{ gateway_addr }}' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: false + register: result + ignore_errors: true + + - name: Issue-28 | Assert connection succeeded + assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Issue-28 | Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ item }}' + host: '{{ gateway_addr }}' + state: absent + with_items: + - "{{ user_name_1 }}" + - "{{ user_name_2 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml new file mode 100644 index 000000000..c95acc292 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml @@ -0,0 +1,84 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Issue-29511 | test setup | drop database + mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: absent + loop: + - foo + - bar + + - name: Issue-29511 | test setup | create database + mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: present + loop: + - foo + - bar + + - name: Issue-29511 | Copy SQL scripts to remote + copy: + src: "{{ item }}" + dest: "{{ remote_tmp_dir }}/{{ item | basename }}" + with_items: + - create-function.sql + - create-procedure.sql + + - name: Issue-29511 | Create function for test + shell: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-function.sql" + + - name: Issue-29511 | Create procedure for test + shell: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-procedure.sql" + + - name: Issue-29511 | Create user with FUNCTION and PROCEDURE privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + state: present + priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' + register: result + + - name: Issue-29511 | Assert Create user with FUNCTION and PROCEDURE privileges + assert: + that: + - result is success + - result is changed + + - name: Issue-29511 | Create user with FUNCTION and PROCEDURE privileges - Idempotent check + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + state: present + priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' + register: result + + - name: Issue-29511 | Assert Create user with FUNCTION and PROCEDURE privileges + assert: + that: + - result is success + - result is not changed + + - name: Issue-29511 | Test teardown | cleanup databases + mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: absent + loop: + - foo + - bar + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml new file mode 100644 index 000000000..a7657f8ab --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml @@ -0,0 +1,52 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Issue-64560 | Set root password + mysql_user: + <<: *mysql_params + name: root + host: '%' + password: '{{ root_password }}' + check_implicit_admin: yes + register: result + + - name: Issue-64560 | Assert root password is changed + assert: + that: + - result is changed + + - name: Issue-64560 | Set root password again + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ root_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: root + host: '%' + password: '{{ root_password }}' + check_implicit_admin: yes + register: result + + - name: Issue-64560 | Assert root password is not changed + assert: + that: + - result is not changed + + - name: Issue-64560 | Set root password again + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ root_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: root + host: '%' + password: '{{ mysql_password }}' + check_implicit_admin: yes + register: result diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/main.yml new file mode 100644 index 000000000..dc5c9d3cd --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -0,0 +1,288 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# test code for the mysql_user module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 dof the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +# create mysql user and verify user is added to mysql database +# + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - include_tasks: issue-121.yml + + - include_tasks: issue-28.yml + + - include_tasks: test_resource_limits.yml + + - include_tasks: test_idempotency.yml + + # ============================================================ + # Create user with no privileges and verify default privileges are assign + # + - name: create user with DEFAULT privilege state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: present + register: result + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost + priv: USAGE + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_1 }}" + + + # ============================================================ + # Create user with select privileges and verify select privileges are assign + # + - name: Create user with SELECT privilege state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_2 }}" + password: "{{ user_password_2 }}" + state: present + priv: '*.*:SELECT' + register: result + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_2 }}" + user_host: localhost + priv: SELECT + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_2 }}" + + + # ============================================================ + # Assert user has access to multiple databases + # + - name: Give users access to multiple databases + mysql_user: + <<: *mysql_params + name: '{{ item[0] }}' + priv: '{{ item[1] }}.*:ALL' + append_privs: yes + password: '{{ user_password_1 }}' + with_nested: + - ['{{ user_name_1 }}', '{{ user_name_2 }}'] + - "{{db_names}}" + + - name: Show grants access for user1 on multiple database + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" + register: result + + - name: Assert grant access for user1 on multiple database + assert: + that: + - "'{{ item }}' in result.stdout" + with_items: "{{ db_names }}" + + - name: Show grants access for user2 on multiple database + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" + register: result + + - name: Assert grant access for user2 on multiple database + assert: + that: + - "'{{ item }}' in result.stdout" + with_items: "{{db_names}}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + + - name: Give user SELECT access to database via wildcard + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + priv: '%db.*:SELECT' + append_privs: yes + password: '{{ user_password_1 }}' + + - name: Show grants access for user1 on database via wildcard + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" + register: result + + - name: assert grant access for user1 on multiple database + assert: + that: + - "'%db' in result.stdout" + - "'SELECT' in result.stdout" + + - name: test priv type check, must fail + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + priv: + - unsuitable + - type + append_privs: yes + host_all: yes + password: '{{ user_password_1 }}' + register: result + ignore_errors: yes + + - name: check fail message + assert: + that: + - result is failed + - result.msg is search('priv parameter must be str or dict') + + - name: Change SELECT to INSERT for user access to database via wildcard + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + priv: '%db.*:INSERT' + append_privs: yes + host_all: yes + password: '{{ user_password_1 }}' + + - name: Show grants access for user1 on database via wildcard + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" + register: result + + - name: assert grant access for user1 on multiple database + assert: + that: + - "'%db' in result.stdout" + - "'INSERT' in result.stdout" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_1}}" + + # ============================================================ + # Test plaintext and encrypted password scenarios. + # + - include_tasks: test_user_password.yml + + # ============================================================ + # Test plugin authentication scenarios. + # + # FIXME: mariadb sql syntax for create/update user is not compatible + - include_tasks: test_user_plugin_auth.yml + when: db_engine == 'mysql' + + # ============================================================ + # Assert create user with SELECT privileges, attempt to create database and update privileges to create database + # + - include_tasks: test_privs.yml + vars: + current_privilege: SELECT + current_append_privs: no + + # ============================================================ + # Assert creating user with SELECT privileges, attempt to create database and append privileges to create database + # + - include_tasks: test_privs.yml + vars: + current_privilege: DROP + current_append_privs: yes + + # ============================================================ + # Assert create user with SELECT privileges, attempt to create database and update privileges to create database + # + - include_tasks: test_privs.yml + vars: + current_privilege: 'UPDATE,ALTER' + current_append_privs: no + + # ============================================================ + # Assert creating user with SELECT privileges, attempt to create database and append privileges to create database + # + - include_tasks: test_privs.yml + vars: + current_privilege: 'INSERT,DELETE' + current_append_privs: yes + + # Tests for the priv parameter with dict value (https://github.com/ansible/ansible/issues/57533) + - include_tasks: test_priv_dict.yml + + # Test that append_privs will not attempt to make a change where current privileges are a superset of new privileges + # (https://github.com/ansible-collections/community.mysql/issues/69) + - include_tasks: test_priv_append.yml + vars: + enable_check_mode: no + - include_tasks: test_priv_append.yml + vars: + enable_check_mode: yes + + # Test that subtract_privs will only revoke the grants given by priv + # (https://github.com/ansible-collections/community.mysql/issues/331) + - include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: no + - include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: yes + + - import_tasks: test_privs_issue_465.yml + tags: + - issue_465 + + # Tests for the TLS requires dictionary + - include_tasks: test_tls_requirements.yml + + - import_tasks: issue-29511.yaml + tags: + - issue-29511 + + - import_tasks: issue-64560.yaml + tags: + - issue-64560 + + # Test that mysql_user still works with force_context enabled (database set to "mysql") + # (https://github.com/ansible-collections/community.mysql/issues/265) + - include_tasks: issue-265.yml + + # https://github.com/ansible-collections/community.mysql/issues/231 + - include_tasks: test_user_grants_with_roles_applied.yml + + - include_tasks: test_revoke_only_grant.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml new file mode 100644 index 000000000..fb601396c --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml @@ -0,0 +1,90 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + # ======================================================================== + # Creation + # ======================================================================== + - include_tasks: utils/create_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_password: "{{ user_password_1 }}" + + - name: Idempotency | Create user that already exist (expect changed=false) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: present + register: result + + - name: Idempotency | Assert create user task is not changed + assert: {that: [result is not changed]} + + # ======================================================================== + # Removal + # ======================================================================== + - name: Idempotency | Remove user (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + state: absent + register: result + + - name: Idempotency | Assert remove user task is changed + ansible.builtin.assert: + that: + - result is changed + + - name: Idempotency | Remove user that doesn't exists (expect changed=false) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + state: absent + register: result + + - name: Idempotency | Assert remove user task is not changed + ansible.builtin.assert: + that: + - result is not changed + + # ======================================================================== + # Removal with host_all + # ======================================================================== + + # Create blank user to be removed later + - include_tasks: utils/create_user.yml + vars: + user_name: "" + user_password: 'KJFDY&D*Sfuysf' + + - name: Idempotency | Remove blank user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + register: result + + - name: Idempotency | Assert removing all blank users is changed + ansible.builtin.assert: + that: + - result is changed + + - name: Idempotency | Remove blank user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + register: result + + - name: Idempotency | Assert removing all blank users is not changed + ansible.builtin.assert: + that: + - result is not changed diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml new file mode 100644 index 000000000..76b4ab12f --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml @@ -0,0 +1,136 @@ +--- +# Test code to ensure that appending privileges will not result in unnecessary changes when the current privileges +# are a superset of the new privileges that have been defined. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Priv append | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - name: Priv append | Create a user with an initial set of privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT,INSERT/data2.*:SELECT,DELETE' + state: present + + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv append | Assert that the initial set of privileges matches what is expected + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" + + - name: Priv append | Append privileges that are a subset of the current privileges, which should be a no-op + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT/data2.*:SELECT' + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv append | Assert that there wasn't a change in permissions + assert: + that: + - result is not changed + + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv append | Assert that the permissions still match what was originally granted + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" + + - name: Priv append | Append privileges that are not included in the current set of privileges to test that privileges are updated + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:DELETE/data2.*:SELECT' + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv append | Assert that there was a change because permissions were added to data1.* + assert: + that: + - result is changed + + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv append | Assert that the permissions were changed as expected if check_mode is set to 'no' + assert: + that: + - "'GRANT SELECT, INSERT, DELETE ON `data1`.*' in result.stdout" + - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Priv append | Assert that the permissions were not actually changed if check_mode is set to 'yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv append | Try to append invalid privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:INVALID/data2.*:SELECT' + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + ignore_errors: true + + - name: Priv append | Assert that there wasn't a change in privileges if check_mode is set to 'no' + assert: + that: + - result is failed + - "'Error granting privileges' in result.msg" + when: enable_check_mode == 'no' + + ########## + # Clean up + - name: Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_4 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml new file mode 100644 index 000000000..f162f6b15 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml @@ -0,0 +1,156 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # Tests for priv parameter value passed as a dict + - name: Priv dict | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + - data3 + + - name: Priv dict | Create user with privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + password: '{{ user_password_3 }}' + priv: + "data1.*": "SELECT" + "data2.*": "SELECT" + state: present + + - name: Priv dict | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: Assert user has giving privileges + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + - "'GRANT SELECT ON `data2`.*' in result.stdout" + + # Issue https://github.com/ansible-collections/community.mysql/issues/99 + - name: Priv dict | Create test table test_table_issue99 + mysql_query: + <<: *mysql_params + query: "CREATE TABLE IF NOT EXISTS data3.test_table_issue99 (a INT, b INT, c INT)" + + - name: Priv dict | Grant select on a column + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a)' + register: result + + - name: Priv dict | Assert that select on a column is changed + assert: + that: + - result is changed + + - name: Priv dict | Grant select on the column again + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a)' + register: result + + - name: Priv dict | Assert that select on the column is not changed + assert: + that: + - result is not changed + + - name: Priv dict | Grant select on columns + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a, b),INSERT' + register: result + + - name: Priv dict | Assert select on columns is changed + assert: + that: + - result is changed + + - name: Priv dict | Grant select on columns again + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a, b),INSERT' + register: result + + - name: Priv dict | Assert that select on columns again is not changed + assert: + that: + - result is not changed + + - name: Priv dict | Grant privs on columns + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a, b), INSERT (a, b), UPDATE' + register: result + + - name: Priv dict | Assert that grant privs on columns is changed + assert: + that: + - result is changed + + - name: Priv dict | Grant same privs on columns again, note that the column order is different + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + 'data3.test_table_issue99': 'SELECT (a, b), UPDATE, INSERT (b, a)' + register: result + + - name: Priv dict | Assert that grants same privs with different order is not changed + assert: + that: + - result is not changed + + - name: Priv dict | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: Priv dict | Assert user has giving privileges + assert: + that: + - "'GRANT SELECT (`A`, `B`), INSERT (`A`, `B`), UPDATE' in result.stdout" + when: "'(`A`, `B`)' in result.stdout" + + - name: Priv dict | Assert user has giving privileges + assert: + that: + - "'GRANT SELECT (A, B), INSERT (A, B), UPDATE' in result.stdout" + when: "'(`A`, `B`)' not in result.stdout" + + ########## + # Clean up + - name: Priv dict | Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + - data3 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_3 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml new file mode 100644 index 000000000..c63396aa0 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml @@ -0,0 +1,177 @@ +--- +# Test code to ensure that subtracting privileges will not result in unnecessary changes. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Priv substract | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + + - name: Priv substract | Create a user with an initial set of privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT,INSERT' + state: present + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the initial set of privileges matches what is expected + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Priv substract | Subtract privileges that are not in the current privileges, which should be a no-op + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there wasn't a change in permissions + assert: + that: + - result is not changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions still match what was originally granted + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Priv substract | Subtract existing and not-existing privileges, but not all + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:INSERT,DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there was a change because permissions were/would be revoked on data1.* + assert: + that: + - result is changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions were not changed if check_mode is set to 'yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that only DELETE was revoked if check_mode is set to 'no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Priv substract | Try to subtract invalid privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:INVALID' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there was no change because invalid permissions are ignored + assert: + that: + - result is not changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions were not changed with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that the permissions were not changed with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Priv substract | Trigger failure by trying to subtract and append privileges at the same time + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT' + subtract_privs: yes + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + ignore_errors: true + + - name: Priv substract | Assert the previous execution failed + assert: + that: + - result is failed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions stayed the same, with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that the permissions stayed the same, with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + ########## + # Clean up + - name: Priv substract | Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_4 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs.yml new file mode 100644 index 000000000..95d44aaa3 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs.yml @@ -0,0 +1,270 @@ +--- +# test code for privileges for mysql_user module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ============================================================ + - name: Privs | Create user with basic select privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: '*.*:SELECT' + state: present + when: current_append_privs == "yes" + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_2 }}" + user_host: "%" + priv: 'SELECT' + when: current_append_privs == "yes" + + - name: Privs | Create user with current privileges (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: '*.*:{{ current_privilege }}' + append_privs: '{{ current_append_privs }}' + state: present + register: result + + - name: Privs | Assert output message for current privileges + assert: + that: + - result is changed + + - name: Privs | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{user_name_2}}'@'%'\"" + register: result + + - name: Privs | Assert user has correct privileges + assert: + that: + - "'GRANT {{ current_privilege | replace(',', ', ') }} ON *.*' in result.stdout" + when: current_append_privs == "no" + + - name: Privs | Assert user has correct privileges + assert: + that: + - "'GRANT SELECT, {{ current_privilege | replace(',', ', ') }} ON *.*' in result.stdout" + when: current_append_privs == "yes" + + - name: Privs | Create database using user current privileges + mysql_db: + login_user: '{{ user_name_2 }}' + login_password: '{{ user_password_2 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + ignore_errors: true + + - name: Privs | Run command to test that database was not created + command: "{{ mysql_command }} -e \"show databases like '{{ db_name }}'\"" + register: result + + - name: Privs | Assert database was not created + assert: + that: + - db_name not in result.stdout + + # ============================================================ + - name: Privs | Add privs to a specific table (expect changed) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: 'jmainguy.jmainguy:ALL' + state: present + register: result + + - name: Privs | Assert that priv changed + assert: + that: + - result is changed + + - name: Privs | Add privs to a specific table (expect ok) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: 'jmainguy.jmainguy:ALL' + state: present + register: result + + - name: Privs | Assert that priv did not change + assert: + that: + - result is not changed + + # ============================================================ + - name: Privs | Grant ALL to user {{ user_name_2 }} + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: '*.*:ALL' + state: present + + # - include_tasks: utils/assert_user.yml user_name={{user_name_2}} user_host=% priv='ALL PRIVILEGES' + + - name: Privs | Create database using user {{ user_name_2 }} + mysql_db: + login_user: '{{ user_name_2 }}' + login_password: '{{ user_password_2 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + + - name: Privs | Run command to test database was created using user new privileges + command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE {{ db_name }}\"" + + - name: Privs | Drop database using user {{ user_name_2 }} + mysql_db: + login_user: '{{ user_name_2 }}' + login_password: '{{ user_password_2 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: absent + + # ============================================================ + - name: Privs | Update user with a long privileges list (mysql has a special multiline grant output) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: '*.*:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER,CREATE,DROP,EVENT,LOCK TABLES,INSERT,UPDATE,DELETE,SELECT,SHOW VIEW,GRANT' + state: present + register: result + + - name: Privs | Assert that priv changed + assert: + that: + - result is changed + + - name: Privs | Test idempotency with a long privileges list (expect ok) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + priv: '*.*:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER,CREATE,DROP,EVENT,LOCK TABLES,INSERT,UPDATE,DELETE,SELECT,SHOW VIEW,GRANT' + state: present + register: result + + # FIXME: on mysql >=8 and mariadb >=10.5.2 there's always a change because + # the REPLICATION CLIENT privilege was renamed to BINLOG MONITOR + - name: Privs | Assert that priv did not change + assert: + that: + - result is not changed + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + + # ============================================================ + - name: Privs | Grant all privileges with grant option + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL,GRANT' + state: present + register: result + + - name: Privs | Assert that priv changed + assert: + that: + - result is changed + + - name: Privs | Collect user info by host + community.mysql.mysql_info: + <<: *mysql_params + filter: "users" + register: mysql_info_about_users + + - name: Privs | Assert that 'GRANT' permission is present + assert: + that: + - mysql_info_about_users.users.localhost.{{ user_name_2 }}.Grant_priv == 'Y' + + - name: Privs | Test idempotency (expect ok) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL,GRANT' + state: present + register: result + + # FIXME: on mysql >=8 there's always a change (ALL PRIVILEGES -> specific privileges) + - name: Privs | Assert that priv did not change + assert: + that: + - result is not changed + + - name: Privs | Collect user info by host + community.mysql.mysql_info: + <<: *mysql_params + filter: "users" + register: mysql_info_about_users + + - name: Privs | Assert that 'GRANT' permission is present (by host) + assert: + that: + - mysql_info_about_users.users.localhost.{{ user_name_2 }}.Grant_priv == 'Y' + + # ============================================================ + - name: Privs | Update user with invalid privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:INVALID' + state: present + register: result + ignore_errors: yes + + - name: Privs | Assert that priv did not change + assert: + that: + - result is failed + - "'Error granting privileges' in result.msg" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml new file mode 100644 index 000000000..2e6a41e89 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml @@ -0,0 +1,31 @@ +--- +# test code for privileges for mysql_user module - issue 465 + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ============================================================ + - name: Privs issue 465 | Create a user with parameters that will always cause an exception + mysql_user: + <<: *mysql_params + name: user_issue_465 + password: a_test_password_465 + priv: '*.{{ db_name }}:SELECT' + state: present + ignore_errors: true + register: result + + - name: Privs issue 465 | Assert output message for current privileges + assert: + that: + - result is failed + - result.msg is search('invalid priv string') + - result.msg is search('params') + - result.msg is search('query') + - result.msg is search('exception') diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml new file mode 100644 index 000000000..a390a4e86 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml @@ -0,0 +1,279 @@ +--- +# test code for resource_limits parameter +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Resource limits | Drop mysql user {{ user_name_1 }} if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + + - name: Resource limits | Create mysql user {{ user_name_1 }} with resource limits in check_mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + check_mode: yes + register: result + + - name: Resource limits | Assert that create user with resource limits is changed + assert: + that: + - result is changed + + - name: Resource limits | Create mysql user {{ user_name_1 }} with resource limits in actual mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - assert: + that: + - result is changed + + - name: Resource limits | Retrieve user + mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 10 + AND max_connections = 5 + register: result + + - name: Resource limits | Assert that rowcount is 1 + assert: + that: + - result.rowcount[0] == 1 + + - name: Resource limits | Try to set the same limits again in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + check_mode: yes + register: result + + - name: Resource limits | Assert that set same limits again is not changed + assert: + that: + - result is not changed + + - name: Resource limits | Try to set the same limits again in actual mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - name: Resource limits | Assert that set same limits again in actual mode is not changed + assert: + that: + - result is not changed + + - name: Resource limits | Change limits + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 5 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - name: Resource limits | Assert limits changed + assert: + that: + - result is changed + + - name: Resource limits | Get user limits + mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 5 + AND max_connections = 5 + register: result + + - name: Resource limits | Assert limit row count + assert: + that: + - result.rowcount[0] == 1 + + - name: Resource limits | Drop mysql user {{ user_name_1 }} if exists + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + + - name: Resource limits | Create mysql user {{ user_name_1 }} with MAX_STATEMENT_TIME in check_mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + check_mode: true + register: result + ignore_errors: true + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is changed for mariadb + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is failed for mysql + ansible.builtin.assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Resource limits | Create mysql user {{ user_name_1 }} with MAX_STATEMENT_TIME in actual mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + register: result + ignore_errors: true + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is changed for MariaDB + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is failed for MySQL + ansible.builtin.assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Resource limits | Retrieve user with MAX_STATEMENT_TIME + community.mysql.mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 10 + AND max_statement_time = 1 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that rowcount is 1 with MAX_STATEMENT_TIME + ansible.builtin.assert: + that: + - result.rowcount[0] == 1 + when: db_engine == 'mariadb' + + - name: Resource limits | Try to set the same limits with MAX_STATEMENT_TIME again in check mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + check_mode: true + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that set same limits with MAX_STATEMENT_TIME again is not changed + ansible.builtin.assert: + that: + - result is not changed + when: db_engine == 'mariadb' + + - name: Resource limits | Try to set the same limits with MAX_STATEMENT_TIME again in actual mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that set same limits with MAX_STATEMENT_TIME again in actual mode is not changed + ansible.builtin.assert: + that: + - result is not changed + when: db_engine == 'mariadb' + + - name: Resource limits | Change limits with MAX_STATEMENT_TIME + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 5 + MAX_STATEMENT_TIME: 2 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert limits with MAX_STATEMENT_TIME changed + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Get user limits with MAX_STATEMENT_TIME + community.mysql.mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 5 + AND max_statement_time = 2 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert limit with MAX_STATEMENT_TIME row count + ansible.builtin.assert: + that: + - result.rowcount[0] == 1 + when: db_engine == 'mariadb' + + when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18') or (ansible_distribution == 'CentOS' and ansible_distribution_major_version >= '8') diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml new file mode 100644 index 000000000..b19227375 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml @@ -0,0 +1,54 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + block: + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" + + - name: Revoke only grants | Create user with two grants + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT,GRANT' + + - name: Revoke only grants | Revoke grant priv from db_user1 + register: result + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT' + + - name: Revoke only grants | Assert that db_user1 only have one priv left + assert: + that: + - result is not failed + - result is changed + + - name: Revoke only grants | Update db_user1 again to test idempotence + register: result + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT' + + - name: Revoke only grants | Assert that task is idempotent + assert: + that: + - result is succeeded + - result is not changed + + always: + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml new file mode 100644 index 000000000..d8c2935a8 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml @@ -0,0 +1,199 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Tls reqs | Create user with TLS requirements in check mode (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + tls_requires: + SSL: + check_mode: yes + register: result + + - name: Tls reqs | Assert check mode user create reports changed state + assert: + that: + - result is changed + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" + + - name: Tls reqs | Create user with TLS requirements state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ item[0] }}' + password: '{{ user_password_1 }}' + tls_requires: '{{ item[1] }}' + with_together: + - [ '{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + - + - SSL: + - X509: + - subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + cipher: 'ECDHE-ECDSA-AES256-SHA384' + issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + + - block: + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ item }}'@'localhost'\"" + register: old_result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: Tls reqs | Set old database separator + set_fact: + separator: '\n' + # Semantically: when mysql version <= 5.6 or MariaDB version <= 10.1 + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.1', '<=')) + + - block: + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ item }}'@'localhost'\"" + register: new_result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: Tls reqs | Set new database separator + set_fact: + separator: 'PASSWORD' + # Semantically: when mysql version >= 5.7 or MariaDB version >= 10.2 + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - block: + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: + - "'SSL' in reqs" + vars: + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + + - name: Tls reqs | Assert user2 TLS requirements + assert: + that: + - "'X509' in reqs" + vars: + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + + - name: Tls reqs | Assert user3 TLS requirements + assert: + that: + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" + vars: + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" + # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. + when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' + + - name: Tls reqs | Modify user with TLS requirements state=present in check mode (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + X509: + check_mode: yes + register: result + + - name: Tls reqs | Assert check mode user update reports changed state + assert: + that: + - result is changed + + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" + register: old_result + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '<')) + + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: new_result + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - name: Tls reqs | Assert user1 TLS requirements was not changed + assert: + that: "'SSL' in reqs" + vars: + - reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + + - name: Tls reqs | Modify user with TLS requirements state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + X509: + + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" + register: old_result + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '<')) + + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: new_result + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: "'X509' in reqs" + vars: + - reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + + - name: Tls reqs | Remove TLS requirements from user (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + + - name: Tls reqs | Retrieve TLS requirements for users + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: result + + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: "'REQUIRE ' not in result.stdout or 'REQUIRE NONE' in result.stdout" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_1}}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_2}}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_3}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_2}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_3}}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml new file mode 100644 index 000000000..428c1ef78 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml @@ -0,0 +1,129 @@ +--- +# Tests scenarios for both plaintext and encrypted user passwords. + +- vars: + mysql_parameters: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + test_password1: kbB9tcx5WOGVGfzV + test_password1_hash: '*AF6A7F9D038475C17EE46564F154104877EE5037' + test_password2: XBYjpHmjIctMxl1y + test_password2_hash: '*9E22D1B35C68BDDF398B8F28AE482E5A865BAC0A' + test_password3: tem33JfR5Yx98BB + test_password3_hash: '*C7E7C2710702F20336F8D93BC0670C8FB66BDBC7' + + + block: + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: "%" + update_password: "{{ item.update_password }}" + password: "{{ test_password1 }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_change }}" + expect_password_hash: "{{ test_password1_hash }}" + loop: + # all variants set the password when nothing exists + - username: test1 + update_password: always + expect_change: true + - username: test2 + update_password: on_create + expect_change: true + - username: test3 + update_password: on_new_username + expect_change: true + + # assert idempotency + - username: test1 + update_password: always + expect_change: false + - username: test2 + update_password: on_create + expect_change: false + - username: test3 + update_password: on_new_username + expect_change: false + + # same user, new password + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: "%" + update_password: "{{ item.update_password }}" + password: "{{ test_password2 }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_change }}" + expect_password_hash: "{{ item.expect_password_hash }}" + loop: + - username: test1 + update_password: always + expect_change: true + expect_password_hash: "{{ test_password2_hash }}" + - username: test2 + update_password: on_create + expect_change: false + expect_password_hash: "{{ test_password1_hash }}" + - username: test3 + update_password: on_new_username + expect_change: false + expect_password_hash: "{{ test_password1_hash }}" + + # new user, new password + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: '::1' + update_password: "{{ item.update_password }}" + password: "{{ item.password }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_password_change }}" + expect_password_hash: "{{ item.expect_password_hash }}" + loop: + - username: test1 + update_password: always + expect_change: true + expect_password_change: true + password: "{{ test_password1 }}" + expect_password_hash: "{{ test_password1_hash }}" + - username: test2 + update_password: on_create + expect_change: true + expect_password_change: true + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password2_hash }}" + - username: test3 + update_password: on_new_username + expect_change: true + expect_password_change: false + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password1_hash }}" + + # prepare for next test: ensure all users have varying passwords + - username: test3 + update_password: always + expect_change: true + expect_password_change: true + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password2_hash }}" + + # another new user, another new password and multiple existing users with varying passwords + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: '2001:db8::1' + update_password: "{{ item.update_password }}" + password: "{{ test_password3 }}" + expect_change: true + expect_password_change: true + expect_password_hash: "{{ test_password3_hash }}" + loop: + - username: test1 + update_password: always + - username: test2 + update_password: on_create + - username: test3 + update_password: on_new_username diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml new file mode 100644 index 000000000..c9714b7f9 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml @@ -0,0 +1,92 @@ +--- +# https://github.com/ansible-collections/community.mysql/issues/231 +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: User grants with roles applied | Skip unsupported versions + meta: end_play + when: + - db_engine == 'mysql' + - db_version is version('8.0.0', '<') + + - name: User grants with roles applied | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - name: User grants with roles applied | Create user with privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + password: '{{ user_password_3 }}' + priv: + "data1.*": "SELECT" + "data2.*": "SELECT" + state: present + + - name: User grants with roles applied | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: Assert user has giving privileges + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + - "'GRANT SELECT ON `data2`.*' in result.stdout" + + - name: User grants with roles applied | Create role + mysql_role: + <<: *mysql_params + name: test231 + members: + - '{{ user_name_3 }}@localhost' + + - name: User grants with roles applied | Try to change privs + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + "data1.*": "INSERT" + "data2.*": "INSERT" + state: present + + - name: User grants with roles applied | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: User grants with roles applied | Assert user has giving privileges + assert: + that: + - "'GRANT INSERT ON `data1`.*' in result.stdout" + - "'GRANT INSERT ON `data2`.*' in result.stdout" + + ########## + # Clean up + - name: User grants with roles applied | Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - data1 + - data2 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_3 }}" + + - name: User grants with roles applied | Drop test role + mysql_role: + <<: *mysql_params + name: test231 + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml new file mode 100644 index 000000000..cffc052c4 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml @@ -0,0 +1,305 @@ +--- +# Tests scenarios for both plaintext and encrypted user passwords. + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + test_user_name: 'test_user_password' + initial_password: 'a5C8SN*DBa0%a75sGz' + initial_password_encrypted: '*0A12D4DF68C2A50716111674E565CA3D7D68B048' + new_password: 'NkN&qECv33vuQzf3bJg' + new_password_encrypted: '*B6559186FAD0953589F54383AD8EE9E9172296DA' + test_default_priv_type: 'SELECT' + test_default_priv: '*.*:{{ test_default_priv_type }}' + + block: + + # ============================================================ + # Test setting plaintext password and changing it. + # + + - name: Password | Create user with initial password + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + password: '{{ initial_password }}' + priv: '{{ test_default_priv }}' + state: present + register: result + + - name: Password | Assert that a change occurred because the user was added + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Password | Get the MySQL version using the newly created used creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ initial_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Password | Run mysql_user again without any changes + mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + password: "{{ initial_password }}" + priv: "{{ test_default_priv }}" + state: present + register: result + + - name: Password | Assert that there weren't any changes because username/password didn't change + assert: + that: + - result is not changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Password | Update the user password + mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + password: "{{ new_password }}" + state: present + register: result + + - name: Password | Assert that a change occurred because the password was updated + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Password | Get the MySQL version data using the original password (should fail) + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ initial_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that the mysql_info module failed because we used the old password + assert: + that: + - result is failed + + - name: Password | Get the MySQL version data using the new password (should work) + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ new_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that the mysql_info module succeeded because we used the new password + assert: + that: + - result is succeeded + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test setting a plaintext password and then the same password encrypted to ensure there isn't a change detected. + # + + - name: Password | Create user with initial password + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + password: '{{ initial_password }}' + priv: '{{ test_default_priv }}' + state: present + register: result + + - name: Password | Assert that a change occurred because the user was added + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "localhost" + priv: "{{ test_default_priv_type }}" + + - name: Password | Pass in the same password as before, but in the encrypted form (no change expected) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + password: '{{ initial_password_encrypted }}' + encrypted: yes + priv: '{{ test_default_priv }}' + state: present + register: result + + - name: Password | Assert that there weren't any changes because username/password didn't change + assert: + that: + - result is not changed + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test setting an encrypted password and then the same password in plaintext to ensure there isn't a change. + # + + - name: Password | Create user with initial password + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: "%" + password: '{{ initial_password_encrypted }}' + encrypted: yes + priv: '{{ test_default_priv }}' + state: present + register: result + + - name: Password | Assert that a change occurred because the user was added + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Password | Get the MySQL version data using the new creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ initial_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that the mysql_info module succeeded because we used the new password + assert: + that: + - result is succeeded + + - name: Password | Pass in the same password as before, but in the encrypted form (no change expected) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: "%" + password: '{{ initial_password }}' + state: present + register: result + + - name: Password | Assert that there weren't any changes because username/password didn't change + assert: + that: + - result is not changed + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test setting an empty password. + # + + - name: Password | Create user with empty password + mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + priv: "{{ test_default_priv }}" + state: present + register: result + + - name: Password | Assert that a change occurred because the user was added + assert: + that: + - result is changed + + - name: Password | Get the MySQL version using an empty password for the newly created user + mysql_info: + login_user: "{{ test_user_name }}" + login_password: "" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Password | Get the MySQL version using an non-empty password (should fail) + mysql_info: + login_user: '{{ test_user_name }}' + login_password: 'some_password' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Password | Assert that mysql_info failed + assert: + that: + - result is failed + + - name: Password | Update the user without changing the password + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: "%" + priv: '{{ test_default_priv }}' + state: present + register: result + + - name: Password | Assert that the user wasn't changed because the password is still empty + assert: + that: + - result is not changed + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml new file mode 100644 index 000000000..d8ff04d5d --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml @@ -0,0 +1,477 @@ +--- +# Test user plugin auth scenarios. + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + test_user_name: 'test_user_plugin_auth' + test_plugin_type: 'mysql_native_password' + test_plugin_hash: '*0CB5B86F23FDC24DB19A29B8854EB860CBC47793' + test_plugin_auth_string: 'Fdt8fd^34ds' + test_plugin_new_hash: '*E74368AC90460FA669F6D41BFB7F2A877DB73745' + test_plugin_new_auth_string: 'c$K01LsmK7nJnIR4!h' + test_default_priv_type: 'SELECT' + test_default_priv: '*.*:{{ test_default_priv_type }}' + + block: + + # ============================================================ + # Test plugin auth initially setting a hash and then changing to a different hash. + # + + - name: Plugin auth | Create user with plugin auth (with hash string) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_hash_string: '{{ test_plugin_hash }}' + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information (with hash string) + command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (with hash string) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (with hash string) + assert: + that: + - "'{{ test_plugin_type }}' in show_create_user.stdout" + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Get the MySQL version using the newly created creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Plugin auth | Update the user with a different hash + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_hash_string: '{{ test_plugin_new_hash }}' + register: result + + - name: Plugin auth | Check that the module makes the change because the hash changed + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Getting the MySQL info with the new password should work + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_new_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test plugin auth initially setting a hash and then switching to a plaintext auth string. + # + + - name: Plugin auth | Create user with plugin auth (with hash string) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_hash_string: '{{ test_plugin_hash }}' + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information + command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (with hash string) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (with hash string) + assert: + that: + - "'{{ test_plugin_type }}' in show_create_user.stdout" + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Get the MySQL version using the newly created creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Plugin auth | Update the user with the same hash (no change expected) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_hash_string: '{{ test_plugin_hash }}' + register: result + + # FIXME: on mariadb 10.2 there's always a change + - name: Plugin auth | Check that the module doesn't make a change when the same hash is passed in + assert: + that: + - result is not changed + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in plaintext form + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_auth_string: '{{ test_plugin_auth_string }}' + register: result + + # Expecting a change is currently by design (see comment in source). + - name: Plugin auth | Check that the module did not change the password + assert: + that: + - result is changed + + - name: Plugin auth | Getting the MySQL info should still work + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test plugin auth initially setting a plaintext auth string and then switching to a hash. + # + + - name: Plugin auth | Create user with plugin auth (with auth string) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_auth_string: '{{ test_plugin_auth_string }}' + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information(with auth string) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (with auth string) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (with auth string) + assert: + that: + - test_plugin_type in show_create_user.stdout + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Get the MySQL version using the newly created creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Plugin auth | Update the user with the same auth string + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_auth_string: '{{ test_plugin_auth_string }}' + register: result + + # This is the current expected behavior because there isn't a reliable way to hash the password in the mysql_user + # module in order to be able to compare this password with the stored hash. See the source for more info. + - name: Plugin auth | The module should detect a change even though the password is the same + assert: + that: + - result is changed + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in hash form + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + plugin_hash_string: '{{ test_plugin_hash }}' + register: result + + - name: Plugin auth | Check that the module did not change the password + assert: + that: + - result is not changed + + - name: Plugin auth | Get the MySQL version using the newly created creds + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '{{ test_plugin_auth_string }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test plugin auth with an empty auth string. + # + + - name: Plugin auth | Create user with plugin auth (empty auth string) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information (empty auth string) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (empty auth string) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (empty auth string) + assert: + that: + - "'{{ test_plugin_type }}' in show_create_user.stdout" + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Get the MySQL version using an empty password for the newly created user + mysql_info: + login_user: '{{ test_user_name }}' + login_password: '' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Plugin auth | Assert that mysql_info was successful + assert: + that: + - result is succeeded + + - name: Plugin auth | Get the MySQL version using an non-empty password (should fail) + mysql_info: + login_user: '{{ test_user_name }}' + login_password: 'some_password' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + filter: version + register: result + ignore_errors: true + + - name: Plugin auth | Assert that mysql_info failed + assert: + that: + - result is failed + + - name: Plugin auth | Update the user without changing the auth mechanism + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + host: '%' + plugin: '{{ test_plugin_type }}' + state: present + register: result + + - name: Plugin auth | Assert that the user wasn't changed because the auth string is still empty + assert: + that: + - result is not changed + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test plugin auth switching from one type of plugin to another without an auth string or hash. The only other + # plugins that are loaded by default are sha2*, but these aren't compatible with pymysql < 0.9, so skip these tests + # for those versions. + # + - name: Plugin auth | Test plugin auth switching which doesn't work on pymysql < 0.9 + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9', '>=') + ) + block: + + - name: Plugin auth | Create user with plugin auth (empty auth string) + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + plugin: '{{ test_plugin_type }}' + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information (empty auth string) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (empty auth string) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (empty auth string) + assert: + that: + - test_plugin_type in show_create_user.stdout + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: localhost + priv: "{{ test_default_priv_type }}" + + - name: Plugin auth | Switch user to sha256_password auth plugin + mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + plugin: sha256_password + priv: '{{ test_default_priv }}' + register: result + + - name: Plugin auth | Get user information (sha256_password) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + register: show_create_user + + - name: Plugin auth | Check that the module made a change (sha256_password) + assert: + that: + - result is changed + + - name: Plugin auth | Check that the expected plugin type is set (sha256_password) + assert: + that: + - "'sha256_password' in show_create_user.stdout" + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) + + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: localhost + priv: "{{ test_default_priv_type }}" + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml new file mode 100644 index 000000000..6fc4fbca8 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml @@ -0,0 +1,8 @@ +--- +- name: Utils | Assert no user | Query for user {{ user_name }} + command: "{{ mysql_command }} -e \"SELECT User FROM mysql.user where user='{{ user_name }}'\"" + register: result + +- name: Utils | Assert no user | Assert mysql user is not present + assert: + that: user_name not in result.stdout diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml new file mode 100644 index 000000000..e6bd23fbb --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml @@ -0,0 +1,21 @@ +--- + +- name: Utils | Assert user | Query for user {{ user_name }} + command: "{{ mysql_command }} -e \"SELECT user FROM mysql.user where user='{{ user_name }}'\"" + register: result + +- name: Utils | Assert user | Assert user is present + assert: + that: + - user_name in result.stdout + +- name: Utils | Assert user | Query for privileges of user {{ user_name }} + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name }}'@'{{ user_host }}'\"" + register: result + when: priv is defined + +- name: Utils | Assert user | Assert user has given privileges + ansible.builtin.assert: + that: + - "'GRANT {{ priv }} ON *.*' in result.stdout" + when: priv is defined diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml new file mode 100644 index 000000000..d95e53b8c --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml @@ -0,0 +1,28 @@ +--- +- name: Utils | Assert user password | Apply update_password to {{ username }} + mysql_user: + login_user: '{{ mysql_parameters.login_user }}' + login_password: '{{ mysql_parameters.login_password }}' + login_host: '{{ mysql_parameters.login_host }}' + login_port: '{{ mysql_parameters.login_port }}' + state: present + name: "{{ username }}" + host: "{{ host }}" + password: "{{ password }}" + update_password: "{{ update_password }}" + register: result + +- name: Utils | Assert user password | Assert a change occurred + assert: + that: + - "result.changed | bool == {{ expect_change }} | bool" + - "result.password_changed == {{ expect_password_change }}" + +- name: Utils | Assert user password | Query user {{ username }} + command: "{{ mysql_command }} -BNe \"SELECT plugin, authentication_string FROM mysql.user where user='{{ username }}' and host='{{ host }}'\"" + register: existing_user + +- name: Utils | Assert user password | Assert expect_hash is in user stdout + assert: + that: + - "'mysql_native_password\t{{ expect_password_hash }}' in existing_user.stdout_lines" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml new file mode 100644 index 000000000..b255ec451 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml @@ -0,0 +1,12 @@ +--- + +- name: Utils | Create user {{ user_name }} + mysql_user: + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + name: "{{ user_name }}" + host: "{{ user_host | default(omit) }}" + password: "{{ user_password }}" + state: present diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml new file mode 100644 index 000000000..473ceceeb --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml @@ -0,0 +1,12 @@ +--- + +- name: Utils | Remove user {{ user_name }} + mysql_user: + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + name: "{{ user_name }}" + host_all: true + state: absent + ignore_errors: true diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/defaults/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/defaults/main.yml new file mode 100644 index 000000000..779eeade9 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# defaults file for test_mysql_variables +mysql_user: root +mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' +mysql_primary_port: 3307 + +user_name_1: 'db_user1' +user_password_1: 'gadfFDSdtTU^Sdfuj' diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/meta/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/meta/main.yml new file mode 100644 index 000000000..01ee3db79 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_controller diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml new file mode 100644 index 000000000..a09bcdba4 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml @@ -0,0 +1,25 @@ +# test code to assert message in mysql_variables module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +# Assert message failure and confirm failed=true +# +- name: assert message failure (expect failed=true) + assert: + that: + - output is failed diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml new file mode 100644 index 000000000..e64c5a7ad --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml @@ -0,0 +1,37 @@ +--- +# test code to assert variables in mysql_variables module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +# Assert mysql variable name and value from mysql database +# +- name: Assert output message changed value + assert: + that: + - "output.changed | bool == changed | bool" + +- name: Run mysql command to show variable + command: "{{ mysql_command }} \"-e show variables like '{{ var_name }}'\"" + register: result + +- name: Assert output mysql variable name and value + assert: + that: + - result is changed + - "'{{ var_name }}' in result.stdout" + - "'{{ var_value }}' in result.stdout" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml new file mode 100644 index 000000000..6f26386c2 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml @@ -0,0 +1,40 @@ +# test code to assert variables in mysql_variables module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +# Assert output variable name/value match mysql variable name/value +# +- name: assert output message changed value + assert: + that: + - "output.changed | bool == changed | bool" + +- set_fact: + key_name: "{{ var_name }}" + key_value: "{{ output.msg[0][0] }}" + +- name: run mysql command to show variable + command: "{{ mysql_command }} \"-e show variables like '{{var_name}}'\"" + register: result + +- name: assert output variable info match mysql variable info + assert: + that: + - result is changed + - "key_name in result.stdout" + - "key_value in result.stdout" diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml new file mode 100644 index 000000000..10a915415 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml @@ -0,0 +1,86 @@ +--- +- name: set fact tls_enabled + command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + register: result +- set_fact: + tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + when: tls_enabled + block: + + # ============================================================ + - name: get server certificate + copy: + content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" + dest: /tmp/cert.pem + delegate_to: localhost + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + ignore_errors: yes + + - name: create user with ssl requirement + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + host: '%' + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + tls_requires: + SSL: + + - name: attempt connection with newly created user (expect failure) + mysql_variables: + variable: '{{ set_name }}' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + when: + - connector_name == 'pymysql' + + - assert: + that: + - result is succeeded + when: + - connector_name != 'pymysql' + + - name: attempt connection with newly created user ignoring hostname + mysql_variables: + variable: '{{ set_name }}' + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + ca_cert: /tmp/cert.pem + check_hostname: no + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg + + - name: Drop mysql user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/main.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/main.yml new file mode 100644 index 000000000..052b27918 --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/main.yml @@ -0,0 +1,8 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- import_tasks: mysql_variables.yml + +- include_tasks: issue-28.yml diff --git a/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml new file mode 100644 index 000000000..2d2318e2d --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml @@ -0,0 +1,454 @@ +# test code for the mysql_variables module +# (c) 2014, Wayne Rosario <wrosario@ansible.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# ============================================================ +# Verify mysql_variable successfully queries a variable +# +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - set_fact: + set_name: 'version' + + - name: read mysql variables (expect changed=false) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + register: result + + - include_tasks: assert_var_output.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + + # ============================================================ + # Verify mysql_variable successfully updates a variable (issue:4568) + # + - set_fact: + set_name: 'delay_key_write' + set_value: 'ON' + + - name: set mysql variable + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + + - name: update mysql variable to same value (expect changed=false) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + register: result + + - include_tasks: assert_var.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: "{{ set_value }}" + + # ============================================================ + # Verify mysql_variable successfully updates a variable using single quotes + # + - set_fact: + set_name: 'wait_timeout' + set_value: '300' + + - name: set mysql variable to a temp value + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '200' + + - name: update mysql variable value (expect changed=true) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + register: result + + - assert: + that: + - result.queries == ["SET GLOBAL `{{ set_name }}` = {{ set_value }}"] + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' + + # ============================================================ + # Verify mysql_variable successfully updates a variable using double quotes + # + - set_fact: + set_name: "wait_timeout" + set_value: "400" + + - name: set mysql variable to a temp value + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: "200" + + - name: update mysql variable value (expect changed=true) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + register: result + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' + + # ============================================================ + # Verify mysql_variable successfully updates a variable using no quotes + # + - set_fact: + set_name: wait_timeout + set_value: 500 + + - name: set mysql variable to a temp value + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: 200 + + - name: update mysql variable value (expect changed=true) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + register: result + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' + + # ============================================================ + # Verify mysql_variable successfully updates a variable using an expression (e.g. 1024*4) + # + - name: set mysql variable value to an expression + mysql_variables: + <<: *mysql_params + variable: max_connect_errors + value: "1024*4" + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'Incorrect argument type to variable' + + # ============================================================ + # Verify mysql_variable fails when setting an incorrect value (out of range) + # + - name: Set mysql variable value to a number out of range + mysql_variables: + <<: *mysql_params + variable: max_connect_errors + value: '-1' + register: oor_result + ignore_errors: true + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ oor_result }}" + var_name: max_connect_errors + var_value: 1 + when: + - connector_name == 'mysqlclient' + - db_engine == 'mysql' # mysqlclient returns "changed" with MariaDB + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ oor_result }}" + msg: 'Truncated incorrect' + when: + - connector_name == 'pymsql' + + # ============================================================ + # Verify mysql_variable fails when setting an incorrect value (incorrect type) + # + - name: set mysql variable value to a non-valid value number + mysql_variables: + <<: *mysql_params + variable: max_connect_errors + value: TEST + register: nvv_result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ nvv_result }}" + msg: 'Incorrect argument type to variable' + + # ============================================================ + # Verify mysql_variable fails when setting an unknown variable + # + - name: set a non mysql variable + mysql_variables: + <<: *mysql_params + variable: my_sql_variable + value: ON + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'Variable not available' + + # ============================================================ + # Verify mysql_variable fails when setting a read-only variable + # + - name: set value of a read only mysql variable + mysql_variables: + <<: *mysql_params + variable: character_set_system + value: utf16 + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'read only variable' + + #============================================================= + # Verify mysql_variable works with the login_user and login_password parameters + # + - set_fact: + set_name: wait_timeout + set_value: 77 + + - name: query mysql_variable using login_user and password_password + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + register: result + + - include_tasks: assert_var_output.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + + - name: set mysql variable to temp value using user login and password (expect changed=true) + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: 20 + register: result + + - name: update mysql variable value using user login and password (expect changed=true) + mysql_variables: + <<: *mysql_params + variable: '{{set_name}}' + value: '{{set_value}}' + register: result + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{result}}" + var_name: "{{set_name}}" + var_value: '{{set_value}}' + + #============================================================ + # Verify mysql_variable fails with an incorrect login_password parameter + # + - set_fact: + set_name: connect_timeout + set_value: 10 + + - name: query mysql_variable using incorrect login_password + mysql_variables: + login_user: '{{ mysql_user }}' + login_password: 'wrongpassword' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + variable: '{{ set_name }}' + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' + + - name: update mysql variable value using incorrect login_password (expect failed=true) + mysql_variables: + login_user: '{{ mysql_user }}' + login_password: 'wrongpassword' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + variable: '{{ set_name }}' + value: '{{ set_value }}' + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' + + #============================================================ + # Verify mysql_variable fails with an incorrect login_host parameter + # + - name: query mysql_variable using incorrect login_host + mysql_variables: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '12.0.0.9' + login_port: '{{ mysql_primary_port }}' + variable: wait_timeout + connect_timeout: 5 + register: result + ignore_errors: true + + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' + + - block: + + #========================================= + # Check mode 'persist' and 'persist_only': + # + - name: update mysql variable value (expect changed=true) in persist mode + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + mode: persist + register: result + + - assert: + that: + - result.queries == ["SET PERSIST `{{ set_name }}` = {{ set_value }}"] + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' + + - name: try to update mysql variable value (expect changed=false) in persist mode again + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + mode: persist + register: result + + - include_tasks: assert_var.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' + + - name: set mysql variable to a temp value + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '200' + mode: persist + + - name: update mysql variable value (expect changed=true) in persist_only mode + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + mode: persist_only + register: result + + - assert: + that: + - result is changed + - result.queries == ["SET PERSIST_ONLY `{{ set_name }}` = {{ set_value }}"] + + - name: try to update mysql variable value (expect changed=false) in persist_only mode again + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + mode: persist_only + register: result + + - assert: + that: + - result is not changed + + - set_fact: + set_name: max_connections + set_value: 105 + def_val: 151 + + - name: update mysql variable value (expect changed=true) in persist_only mode + mysql_variables: + <<: *mysql_params + variable: '{{ set_name }}' + value: '{{ set_value }}' + mode: persist_only + register: result + + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ def_val }}' + + when: + - db_engine == 'mysql' + - db_version is version('8.0', '>=') + + # Bugfix of https://github.com/ansible/ansible/issues/54239 + # - name: set variable containing dot + # mysql_variables: + # <<: *mysql_params + # variable: validate_password.policy + # value: LOW + # mode: persist_only + # register: result + # + # - assert: + # that: + # - result is changed + # - result.queries == ["SET PERSIST_ONLY `validate_password`.`policy` = LOW"] diff --git a/ansible_collections/community/mysql/tests/integration/test_connection.yml b/ansible_collections/community/mysql/tests/integration/test_connection.yml new file mode 100644 index 000000000..160cfba8a --- /dev/null +++ b/ansible_collections/community/mysql/tests/integration/test_connection.yml @@ -0,0 +1,81 @@ +--- + +- name: Playbook to test bug to connect to MySQL/MariaDB server + hosts: all + gather_facts: false + vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + tasks: + + # Create default MySQL config file with credentials + - name: mysql_info - create default config file + template: + src: my.cnf.j2 + dest: /root/.my.cnf + mode: '0400' + + # Create non-default MySQL config file with credentials + - name: mysql_info - create non-default config file + template: + src: tests/integration/targets/test_mysql_info/templates/my.cnf.j2 + dest: /root/non-default_my.cnf + mode: '0400' + + ############### + # Do tests + + # Access by default cred file + - name: mysql_info - collect default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + register: result + + - assert: + that: + - result is not changed + - db_version in result.version.full + - result.settings != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.users != {} + + # Access by non-default cred file + - name: mysql_info - check non-default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + config_file: /root/non-default_my.cnf + register: result + + - assert: + that: + - result is not changed + - result.version != {} + + # Remove cred files + - name: mysql_info - remove cred files + file: + path: '{{ item }}' + state: absent + with_items: + - /root/.my.cnf + - /root/non-default_my.cnf + + # Access with password + - name: mysql_info - check access with password + mysql_info: + <<: *mysql_params + register: result + + - assert: + that: + - result is not changed + - result.version != {} diff --git a/ansible_collections/community/mysql/tests/sanity/ignore-2.12.txt b/ansible_collections/community/mysql/tests/sanity/ignore-2.12.txt new file mode 100644 index 000000000..c0323aff3 --- /dev/null +++ b/ansible_collections/community/mysql/tests/sanity/ignore-2.12.txt @@ -0,0 +1,8 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch diff --git a/ansible_collections/community/mysql/tests/sanity/ignore-2.13.txt b/ansible_collections/community/mysql/tests/sanity/ignore-2.13.txt new file mode 100644 index 000000000..c0323aff3 --- /dev/null +++ b/ansible_collections/community/mysql/tests/sanity/ignore-2.13.txt @@ -0,0 +1,8 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch diff --git a/ansible_collections/community/mysql/tests/sanity/ignore-2.14.txt b/ansible_collections/community/mysql/tests/sanity/ignore-2.14.txt new file mode 100644 index 000000000..c0323aff3 --- /dev/null +++ b/ansible_collections/community/mysql/tests/sanity/ignore-2.14.txt @@ -0,0 +1,8 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch diff --git a/ansible_collections/community/mysql/tests/sanity/ignore-2.15.txt b/ansible_collections/community/mysql/tests/sanity/ignore-2.15.txt new file mode 100644 index 000000000..da0354c97 --- /dev/null +++ b/ansible_collections/community/mysql/tests/sanity/ignore-2.15.txt @@ -0,0 +1,10 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/ansible_collections/community/mysql/tests/sanity/ignore-2.16.txt b/ansible_collections/community/mysql/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..da0354c97 --- /dev/null +++ b/ansible_collections/community/mysql/tests/sanity/ignore-2.16.txt @@ -0,0 +1,10 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/__init__.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/__init__.py diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_replication.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_replication.py new file mode 100644 index 000000000..deb3099ce --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_replication.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb.replication import uses_replica_terminology +from ..utils import dummy_cursor_class + + +@pytest.mark.parametrize( + 'f_output,c_output,c_ret_type', + [ + (False, '10.5.0-mariadb', 'dict'), + (True, '10.5.1-mariadb', 'dict'), + (True, '10.6.0-mariadb', 'dict'), + (True, '11.5.1-mariadb', 'dict'), + ] +) +def test_uses_replica_terminology(f_output, c_output, c_ret_type): + cursor = dummy_cursor_class(c_output, c_ret_type) + assert uses_replica_terminology(cursor) == f_output diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_user_implementation.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_user_implementation.py new file mode 100644 index 000000000..a6fbff9a2 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mariadb_user_implementation.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb.user import ( + supports_identified_by_password, +) +from ..utils import dummy_cursor_class + + +@pytest.mark.parametrize( + 'function_return,cursor_output,cursor_ret_type', + [ + (True, '10.5.0-mariadb', 'dict'), + (True, '10.5.1-mariadb', 'dict'), + (True, '10.6.0-mariadb', 'dict'), + (True, '11.5.1-mariadb', 'dict'), + ] +) +def test_supports_identified_by_password(function_return, cursor_output, cursor_ret_type): + """ + Tests whether 'CREATE USER %s@%s IDENTIFIED BY PASSWORD %s' is supported, + which is currently supported by everything besides MySQL >= 8.0. + """ + cursor = dummy_cursor_class(cursor_output, cursor_ret_type) + assert supports_identified_by_password(cursor) == function_return diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql.py new file mode 100644 index 000000000..ac4de24f4 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql.py @@ -0,0 +1,24 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version +from ..utils import dummy_cursor_class + + +@pytest.mark.parametrize( + 'cursor_return_version,cursor_return_type', + [ + ('5.7.0-mysql', 'dict'), + ('8.0.0-mysql', 'list'), + ('10.5.0-mariadb', 'dict'), + ('10.5.1-mariadb', 'list'), + ] +) +def test_get_server_version(cursor_return_version, cursor_return_type): + """ + Test that server versions are handled properly by get_server_version() whether they're returned as a list or dict. + """ + cursor = dummy_cursor_class(cursor_return_version, cursor_return_type) + assert get_server_version(cursor) == cursor_return_version diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_replication.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_replication.py new file mode 100644 index 000000000..96d4d9ac6 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_replication.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.replication import uses_replica_terminology +from ..utils import dummy_cursor_class + + +@pytest.mark.parametrize( + 'f_output,c_output,c_ret_type', + [ + (False, '5.5.1-mysql', 'list'), + (False, '5.7.0-mysql', 'dict'), + (False, '8.0.0-mysql', 'list'), + (False, '8.0.11-mysql', 'dict'), + (False, '8.0.21-mysql', 'list'), + (True, '8.0.22-mysql', 'list'), + (True, '8.1.2-mysql', 'dict'), + (True, '9.0.0-mysql', 'list'), + ] +) +def test_uses_replica_terminology(f_output, c_output, c_ret_type): + cursor = dummy_cursor_class(c_output, c_ret_type) + assert uses_replica_terminology(cursor) == f_output diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user.py new file mode 100644 index 000000000..46b3b8eb6 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.user import ( + handle_grant_on_col, + has_grant_on_col, + normalize_col_grants, + sort_column_order +) + + +@pytest.mark.parametrize( + 'input_list,grant,output_tuple', + [ + (['INSERT', 'DELETE'], 'INSERT', (None, None)), + (['SELECT', 'UPDATE'], 'SELECT', (None, None)), + (['INSERT', 'UPDATE', 'INSERT', 'DELETE'], 'DELETE', (None, None)), + (['just', 'a', 'random', 'text'], 'blabla', (None, None)), + (['SELECT (A, B, C)'], 'SELECT', (0, 0)), + (['UPDATE', 'SELECT (A, B, C)'], 'SELECT', (1, 1)), + (['UPDATE', 'REFERENCES (A, B, C)'], 'REFERENCES', (1, 1)), + (['SELECT', 'UPDATE (A, B, C)'], 'UPDATE', (1, 1)), + (['INSERT', 'SELECT (A', 'B)'], 'SELECT', (1, 2)), + (['SELECT (A', 'B)', 'UPDATE'], 'SELECT', (0, 1)), + (['INSERT', 'SELECT (A', 'B)', 'UPDATE'], 'SELECT', (1, 2)), + (['INSERT (A, B)', 'SELECT (A', 'B)', 'UPDATE'], 'INSERT', (0, 0)), + (['INSERT (A', 'B)', 'SELECT (A', 'B)', 'UPDATE'], 'INSERT', (0, 1)), + (['INSERT (A', 'B)', 'SELECT (A', 'B)', 'UPDATE'], 'SELECT', (2, 3)), + (['INSERT (A', 'B)', 'SELECT (A', 'C', 'B)', 'UPDATE'], 'SELECT', (2, 4)), + ] +) +def test_has_grant_on_col(input_list, grant, output_tuple): + """Tests has_grant_on_col function.""" + assert has_grant_on_col(input_list, grant) == output_tuple + + +@pytest.mark.parametrize( + 'input_,output', + [ + ('SELECT (A)', 'SELECT (A)'), + ('SELECT (`A`)', 'SELECT (A)'), + ('UPDATE (B, A)', 'UPDATE (A, B)'), + ('INSERT (`A`, `B`)', 'INSERT (A, B)'), + ('REFERENCES (B, A)', 'REFERENCES (A, B)'), + ('SELECT (`B`, `A`)', 'SELECT (A, B)'), + ('SELECT (`B`, `A`, C)', 'SELECT (A, B, C)'), + ] +) +def test_sort_column_order(input_, output): + """Tests sort_column_order function.""" + assert sort_column_order(input_) == output + + +@pytest.mark.parametrize( + 'privileges,start,end,output', + [ + (['UPDATE', 'SELECT (C, B, A)'], 1, 1, ['UPDATE', 'SELECT (A, B, C)']), + (['INSERT', 'SELECT (A', 'B)'], 1, 2, ['INSERT', 'SELECT (A, B)']), + ( + ['SELECT (`A`', 'B)', 'UPDATE', 'REFERENCES (B, A)'], 0, 1, + ['SELECT (A, B)', 'UPDATE', 'REFERENCES (B, A)']), + ( + ['INSERT', 'REFERENCES (`B`', 'A', 'C)', 'UPDATE (A', 'B)'], 1, 3, + ['INSERT', 'REFERENCES (A, B, C)', 'UPDATE (A', 'B)']), + ] +) +def test_handle_grant_on_col(privileges, start, end, output): + """Tests handle_grant_on_col function.""" + assert handle_grant_on_col(privileges, start, end) == output + + +@pytest.mark.parametrize( + 'input_,expected', + [ + (['SELECT'], ['SELECT']), + (['SELECT (A, B)'], ['SELECT (A, B)']), + (['SELECT (B, A)'], ['SELECT (A, B)']), + (['UPDATE', 'SELECT (C, B, A)'], ['UPDATE', 'SELECT (A, B, C)']), + (['INSERT', 'SELECT (A', 'B)'], ['INSERT', 'SELECT (A, B)']), + ( + ['SELECT (`A`', 'B)', 'UPDATE', 'REFERENCES (B, A)'], + ['SELECT (A, B)', 'UPDATE', 'REFERENCES (A, B)']), + ( + ['INSERT', 'REFERENCES (`B`', 'A', 'C)', 'UPDATE (B', 'A)', 'DELETE'], + ['INSERT', 'REFERENCES (A, B, C)', 'UPDATE (A, B)', 'DELETE']), + ] +) +def test_normalize_col_grants(input_, expected): + """Tests normalize_col_grants function.""" + assert normalize_col_grants(input_) == expected diff --git a/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user_implementation.py b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user_implementation.py new file mode 100644 index 000000000..c1fe2ee97 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/module_utils/test_mysql_user_implementation.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.user import ( + supports_identified_by_password, +) +from ..utils import dummy_cursor_class + + +@pytest.mark.parametrize( + 'function_return,cursor_output,cursor_ret_type', + [ + (True, '5.5.1-mysql', 'list'), + (True, '5.7.0-mysql', 'dict'), + (False, '8.0.22-mysql', 'list'), + (False, '8.1.2-mysql', 'dict'), + (False, '9.0.0-mysql', 'list'), + (False, '8.0.0-mysql', 'list'), + (False, '8.0.11-mysql', 'dict'), + (False, '8.0.21-mysql', 'list'), + ] +) +def test_supports_identified_by_password(function_return, cursor_output, cursor_ret_type): + """ + Tests whether 'CREATE USER %s@%s IDENTIFIED BY PASSWORD %s' is supported, + which is currently supported by everything besides MySQL >= 8.0. + """ + cursor = dummy_cursor_class(cursor_output, cursor_ret_type) + assert supports_identified_by_password(cursor) == function_return diff --git a/ansible_collections/community/mysql/tests/unit/plugins/modules/__init__.py b/ansible_collections/community/mysql/tests/unit/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/modules/__init__.py diff --git a/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_info.py b/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_info.py new file mode 100644 index 000000000..7aa9577e5 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_info.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + +from ansible_collections.community.mysql.plugins.modules.mysql_info import MySQL_Info + + +@pytest.mark.parametrize( + 'suffix,cursor_output', + [ + ('mysql', '5.5.1-mysql'), + ('log', '5.7.31-log'), + ('mariadb', '10.5.0-mariadb'), + ('', '8.0.22'), + ] +) +def test_get_info_suffix(suffix, cursor_output): + def __cursor_return_value(input_parameter): + if input_parameter == "SHOW GLOBAL VARIABLES": + cursor.fetchall.return_value = [{"Variable_name": "version", "Value": cursor_output}] + else: + cursor.fetchall.return_value = MagicMock() + + cursor = MagicMock() + cursor.execute.side_effect = __cursor_return_value + + info = MySQL_Info(MagicMock(), cursor) + + assert info.get_info([], [], False)['version']['suffix'] == suffix diff --git a/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_role.py b/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_role.py new file mode 100644 index 000000000..3c24719a2 --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/modules/test_mysql_role.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.modules.mysql_role import ( + MariaDBQueryBuilder, + MySQLQueryBuilder, + normalize_users, +) + +# TODO: Also cover DbServer, Role, MySQLRoleImpl, MariaDBRoleImpl classes + + +class Module(): + def __init__(self): + self.msg = None + + def fail_json(self, msg=None): + self.msg = msg + + +module = Module() + + +@pytest.mark.parametrize( + 'builder,output', + [ + (MariaDBQueryBuilder('role0'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role0',))), + (MySQLQueryBuilder('role0', '%'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role0', '%'))), + (MariaDBQueryBuilder('role1'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role1',))), + (MySQLQueryBuilder('role1', 'fake'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role1', 'fake'))), + ] +) +def test_query_builder_role_exists(builder, output): + """Test role_exists method of the builder classes.""" + assert builder.role_exists() == output + + +@pytest.mark.parametrize( + 'builder,admin,output', + [ + (MariaDBQueryBuilder('role0'), None, ('CREATE ROLE %s', ('role0',))), + (MySQLQueryBuilder('role0', '%'), None, ('CREATE ROLE %s', ('role0',))), + (MariaDBQueryBuilder('role1'), None, ('CREATE ROLE %s', ('role1',))), + (MySQLQueryBuilder('role1', 'fake'), None, ('CREATE ROLE %s', ('role1',))), + (MariaDBQueryBuilder('role0'), ('user0', ''), ('CREATE ROLE %s WITH ADMIN %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('CREATE ROLE %s', ('role0',))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('CREATE ROLE %s WITH ADMIN %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('CREATE ROLE %s', ('role1',))), + ] +) +def test_query_builder_role_create(builder, admin, output): + """Test role_create method of the builder classes.""" + assert builder.role_create(admin) == output + + +@pytest.mark.parametrize( + 'builder,user,output', + [ + (MariaDBQueryBuilder('role0'), ('user0', ''), ('GRANT %s TO %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('GRANT %s@%s TO %s', ('role0', '%', 'user0'))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('GRANT %s TO %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('GRANT %s@%s TO %s@%s', ('role1', 'fake', 'user0', 'localhost'))), + ] +) +def test_query_builder_role_grant(builder, user, output): + """Test role_grant method of the builder classes.""" + assert builder.role_grant(user) == output + + +@pytest.mark.parametrize( + 'builder,user,output', + [ + (MariaDBQueryBuilder('role0'), ('user0', ''), ('REVOKE %s FROM %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('REVOKE %s@%s FROM %s', ('role0', '%', 'user0'))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('REVOKE %s FROM %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('REVOKE %s@%s FROM %s@%s', ('role1', 'fake', 'user0', 'localhost'))), + ] +) +def test_query_builder_role_revoke(builder, user, output): + """Test role_revoke method of the builder classes.""" + assert builder.role_revoke(user) == output + + +@pytest.mark.parametrize( + 'input_,output,is_mariadb', + [ + (['user'], [('user', '')], True), + (['user'], [('user', '%')], False), + (['user@%'], [('user', '%')], True), + (['user@%'], [('user', '%')], False), + (['user@localhost'], [('user', 'localhost')], True), + (['user@localhost'], [('user', 'localhost')], False), + (['user', 'user@%'], [('user', ''), ('user', '%')], True), + (['user', 'user@%'], [('user', '%'), ('user', '%')], False), + ] +) +def test_normalize_users(input_, output, is_mariadb): + """Test normalize_users function with expected input.""" + assert normalize_users(None, input_, is_mariadb) == output + + +@pytest.mark.parametrize( + 'input_,is_mariadb,err_msg', + [ + ([''], True, "Member's name cannot be empty."), + ([''], False, "Member's name cannot be empty."), + ([None], True, "Error occured while parsing"), + ([None], False, "Error occured while parsing"), + ] +) +def test_normalize_users_failing(input_, is_mariadb, err_msg): + """Test normalize_users function with wrong input.""" + + normalize_users(module, input_, is_mariadb) + assert err_msg in module.msg diff --git a/ansible_collections/community/mysql/tests/unit/plugins/utils.py b/ansible_collections/community/mysql/tests/unit/plugins/utils.py new file mode 100644 index 000000000..7712d1cdc --- /dev/null +++ b/ansible_collections/community/mysql/tests/unit/plugins/utils.py @@ -0,0 +1,19 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class dummy_cursor_class(): + """Dummy class for returning an answer for SELECT VERSION().""" + def __init__(self, output, ret_val_type='dict'): + self.output = output + self.ret_val_type = ret_val_type + + def execute(self, query): + pass + + def fetchone(self): + if self.ret_val_type == 'dict': + return {'version': self.output} + + elif self.ret_val_type == 'list': + return [self.output] |