|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright 2021 Canonical Ltd. |
| 3 | +# See LICENSE file for licensing details. |
| 4 | + |
| 5 | +import logging |
| 6 | +from asyncio import TimeoutError |
| 7 | + |
| 8 | +import pytest |
| 9 | +from juju import tag |
| 10 | +from pytest_operator.plugin import OpsTest |
| 11 | +from tenacity import Retrying, stop_after_delay, wait_fixed |
| 12 | + |
| 13 | +from ..helpers import ( |
| 14 | + APPLICATION_NAME, |
| 15 | + CHARM_SERIES, |
| 16 | +) |
| 17 | +from ..juju_ import juju_major_version |
| 18 | +from .helpers import ( |
| 19 | + add_unit_with_storage, |
| 20 | + check_db, |
| 21 | + check_password_auth, |
| 22 | + create_db, |
| 23 | + get_any_deatached_storage, |
| 24 | + is_postgresql_ready, |
| 25 | + is_storage_exists, |
| 26 | + remove_unit_force, |
| 27 | + storage_id, |
| 28 | +) |
| 29 | + |
| 30 | +TEST_DATABASE_NAME = "test_database" |
| 31 | +DUP_APPLICATION_NAME = "postgres-test-dup" |
| 32 | + |
| 33 | +logger = logging.getLogger(__name__) |
| 34 | + |
| 35 | + |
| 36 | +@pytest.mark.group(1) |
| 37 | +@pytest.mark.abort_on_fail |
| 38 | +async def test_app_force_removal(ops_test: OpsTest, charm: str): |
| 39 | + """Remove unit with force while storage is alive.""" |
| 40 | + async with ops_test.fast_forward(): |
| 41 | + # Deploy the charm. |
| 42 | + logger.info("deploying charm") |
| 43 | + await ops_test.model.deploy( |
| 44 | + charm, |
| 45 | + application_name=APPLICATION_NAME, |
| 46 | + num_units=1, |
| 47 | + series=CHARM_SERIES, |
| 48 | + storage={"pgdata": {"pool": "lxd-btrfs", "size": 8046}}, |
| 49 | + config={"profile": "testing"}, |
| 50 | + ) |
| 51 | + |
| 52 | + logger.info("waiting for idle") |
| 53 | + await ops_test.model.wait_for_idle(apps=[APPLICATION_NAME], status="active", timeout=1500) |
| 54 | + assert ops_test.model.applications[APPLICATION_NAME].units[0].workload_status == "active" |
| 55 | + |
| 56 | + primary_name = ops_test.model.applications[APPLICATION_NAME].units[0].name |
| 57 | + |
| 58 | + logger.info("waiting for postgresql") |
| 59 | + for attempt in Retrying(stop=stop_after_delay(15 * 3), wait=wait_fixed(3), reraise=True): |
| 60 | + with attempt: |
| 61 | + assert await is_postgresql_ready(ops_test, primary_name) |
| 62 | + |
| 63 | + logger.info("getting storage id") |
| 64 | + storage_id_str = storage_id(ops_test, primary_name) |
| 65 | + |
| 66 | + # Check if storage exists after application deployed |
| 67 | + logger.info("werifing is storage exists") |
| 68 | + for attempt in Retrying(stop=stop_after_delay(15 * 3), wait=wait_fixed(3), reraise=True): |
| 69 | + with attempt: |
| 70 | + assert await is_storage_exists(ops_test, storage_id_str) |
| 71 | + |
| 72 | + # Create test database to check there is no resources conflicts |
| 73 | + logger.info("creating db") |
| 74 | + await create_db(ops_test, APPLICATION_NAME, TEST_DATABASE_NAME) |
| 75 | + |
| 76 | + # Check that test database is not exists for new unit |
| 77 | + logger.info("checking db") |
| 78 | + assert await check_db(ops_test, APPLICATION_NAME, TEST_DATABASE_NAME) |
| 79 | + |
| 80 | + # Destroy charm |
| 81 | + logger.info("force removing charm") |
| 82 | + if juju_major_version == 2: |
| 83 | + await remove_unit_force(ops_test, primary_name) |
| 84 | + else: |
| 85 | + await ops_test.model.destroy_unit( |
| 86 | + primary_name, force=True, destroy_storage=False, max_wait=1500 |
| 87 | + ) |
| 88 | + |
| 89 | + # Storage should remain |
| 90 | + logger.info("werifing is storage exists") |
| 91 | + for attempt in Retrying(stop=stop_after_delay(15 * 3), wait=wait_fixed(3), reraise=True): |
| 92 | + with attempt: |
| 93 | + assert await is_storage_exists(ops_test, storage_id_str) |
| 94 | + |
| 95 | + |
| 96 | +@pytest.mark.group(1) |
| 97 | +@pytest.mark.abort_on_fail |
| 98 | +async def test_charm_garbage_ignorance(ops_test: OpsTest, charm: str): |
| 99 | + """Test charm deploy in dirty environment with garbage storage.""" |
| 100 | + async with ops_test.fast_forward(): |
| 101 | + logger.info("checking garbage storage") |
| 102 | + garbage_storage = None |
| 103 | + for attempt in Retrying(stop=stop_after_delay(30 * 3), wait=wait_fixed(3), reraise=True): |
| 104 | + with attempt: |
| 105 | + garbage_storage = await get_any_deatached_storage(ops_test) |
| 106 | + |
| 107 | + logger.info("add unit with attached storage") |
| 108 | + await add_unit_with_storage(ops_test, APPLICATION_NAME, garbage_storage) |
| 109 | + |
| 110 | + primary_name = ops_test.model.applications[APPLICATION_NAME].units[0].name |
| 111 | + |
| 112 | + logger.info("waiting for postgresql") |
| 113 | + for attempt in Retrying(stop=stop_after_delay(15 * 3), wait=wait_fixed(3), reraise=True): |
| 114 | + with attempt: |
| 115 | + assert await is_postgresql_ready(ops_test, primary_name) |
| 116 | + |
| 117 | + logger.info("getting storage id") |
| 118 | + storage_id_str = storage_id(ops_test, primary_name) |
| 119 | + |
| 120 | + assert storage_id_str == garbage_storage |
| 121 | + |
| 122 | + # Check if storage exists after application deployed |
| 123 | + logger.info("werifing is storage exists") |
| 124 | + for attempt in Retrying(stop=stop_after_delay(15 * 3), wait=wait_fixed(3), reraise=True): |
| 125 | + with attempt: |
| 126 | + assert await is_storage_exists(ops_test, storage_id_str) |
| 127 | + |
| 128 | + # Check that test database exists for new unit |
| 129 | + logger.info("checking db") |
| 130 | + assert await check_db(ops_test, APPLICATION_NAME, TEST_DATABASE_NAME) |
| 131 | + |
| 132 | + logger.info("removing charm") |
| 133 | + await ops_test.model.destroy_unit(primary_name) |
| 134 | + |
| 135 | + |
| 136 | +@pytest.mark.group(1) |
| 137 | +@pytest.mark.abort_on_fail |
| 138 | +@pytest.mark.skipif(juju_major_version < 3, reason="Requires juju 3 or higher") |
| 139 | +async def test_app_resources_conflicts_v3(ops_test: OpsTest, charm: str): |
| 140 | + """Test application deploy in dirty environment with garbage storage from another application.""" |
| 141 | + async with ops_test.fast_forward(): |
| 142 | + logger.info("checking garbage storage") |
| 143 | + garbage_storage = None |
| 144 | + for attempt in Retrying(stop=stop_after_delay(30 * 3), wait=wait_fixed(3), reraise=True): |
| 145 | + with attempt: |
| 146 | + garbage_storage = await get_any_deatached_storage(ops_test) |
| 147 | + |
| 148 | + logger.info("deploying duplicate application with attached storage") |
| 149 | + await ops_test.model.deploy( |
| 150 | + charm, |
| 151 | + application_name=DUP_APPLICATION_NAME, |
| 152 | + num_units=1, |
| 153 | + series=CHARM_SERIES, |
| 154 | + attach_storage=[tag.storage(garbage_storage)], |
| 155 | + config={"profile": "testing"}, |
| 156 | + ) |
| 157 | + |
| 158 | + # Reducing the update status frequency to speed up the triggering of deferred events. |
| 159 | + await ops_test.model.set_config({"update-status-hook-interval": "10s"}) |
| 160 | + |
| 161 | + logger.info("waiting for duplicate application to be blocked") |
| 162 | + try: |
| 163 | + await ops_test.model.wait_for_idle( |
| 164 | + apps=[DUP_APPLICATION_NAME], timeout=1000, status="blocked" |
| 165 | + ) |
| 166 | + except TimeoutError: |
| 167 | + logger.info("Application is not in blocked state. Checking logs...") |
| 168 | + |
| 169 | + # Since application have postgresql db in storage from external application it should not be able to connect due to new password |
| 170 | + logger.info("checking operator password auth") |
| 171 | + assert not await check_password_auth( |
| 172 | + ops_test, ops_test.model.applications[DUP_APPLICATION_NAME].units[0].name |
| 173 | + ) |
| 174 | + |
| 175 | + |
| 176 | +@pytest.mark.group(1) |
| 177 | +@pytest.mark.abort_on_fail |
| 178 | +@pytest.mark.skipif(juju_major_version != 2, reason="Requires juju 2") |
| 179 | +async def test_app_resources_conflicts_v2(ops_test: OpsTest, charm: str): |
| 180 | + """Test application deploy in dirty environment with garbage storage from another application.""" |
| 181 | + async with ops_test.fast_forward(): |
| 182 | + logger.info("checking garbage storage") |
| 183 | + garbage_storage = None |
| 184 | + for attempt in Retrying(stop=stop_after_delay(30 * 3), wait=wait_fixed(3), reraise=True): |
| 185 | + with attempt: |
| 186 | + garbage_storage = await get_any_deatached_storage(ops_test) |
| 187 | + |
| 188 | + # Deploy duplicaate charm |
| 189 | + logger.info("deploying duplicate application") |
| 190 | + await ops_test.model.deploy( |
| 191 | + charm, |
| 192 | + application_name=DUP_APPLICATION_NAME, |
| 193 | + num_units=1, |
| 194 | + series=CHARM_SERIES, |
| 195 | + config={"profile": "testing"}, |
| 196 | + ) |
| 197 | + |
| 198 | + logger.info("force removing charm") |
| 199 | + await remove_unit_force( |
| 200 | + ops_test, ops_test.model.applications[DUP_APPLICATION_NAME].units[0].name |
| 201 | + ) |
| 202 | + |
| 203 | + # Add unit with garbage storage |
| 204 | + logger.info("adding charm with attached storage") |
| 205 | + add_unit_cmd = f"add-unit {DUP_APPLICATION_NAME} --model={ops_test.model.info.name} --attach-storage={garbage_storage}".split() |
| 206 | + return_code, _, _ = await ops_test.juju(*add_unit_cmd) |
| 207 | + assert return_code == 0, "Failed to add unit with storage" |
| 208 | + |
| 209 | + logger.info("waiting for duplicate application to be blocked") |
| 210 | + try: |
| 211 | + await ops_test.model.wait_for_idle( |
| 212 | + apps=[DUP_APPLICATION_NAME], timeout=1000, status="blocked" |
| 213 | + ) |
| 214 | + except TimeoutError: |
| 215 | + logger.info("Application is not in blocked state. Checking logs...") |
| 216 | + |
| 217 | + # Since application have postgresql db in storage from external application it should not be able to connect due to new password |
| 218 | + logger.info("checking operator password auth") |
| 219 | + assert not await check_password_auth( |
| 220 | + ops_test, ops_test.model.applications[DUP_APPLICATION_NAME].units[0].name |
| 221 | + ) |
0 commit comments