Repository: thelastpickle/cassandra-medusa

Issue:
readme.md disagrees with pyproject.toml
[Project board link](https://github.com/orgs/k8ssandra/projects/8/views/1?pane=issue&itemId=101656385)

readme.md says v0.23.0 (and I think v0.22.3 too) will work on python at least 3.9 and at most 3.12, but according to pyproject it should be in fact between 3.8 and 3.11. I've tried installing with python 3.12 and it didn't work, so I tend to side with pyproject on this:

![Image](https://github.com/user-attachments/assets/06d3792e-c402-492c-b5cd-9d5f8b4f6b6e)

![Image](https://github.com/user-attachments/assets/9d6d5211-b144-4eb4-9bff-81bcbdbf3141)

┆Issue is synchronized with this [Jira Story](https://datastax.jira.com/browse/MED-126) by [Unito](https://www.unito.io)
┆Issue Number: MED-126

Hints from issue comments:
@rzvoncek: Hello @ftarrega !

We've merged #764 which makes the master branch work with python >=3.9,<=3.12. The badges on the "homepage" render off the master branch. Because we don't have a release with this change in it, any released version will have a different python support. 

So you have caught this correctly. Our readme is indeed misleading.

This will get remedied soon though. We are waiting for #854 before a release, and it's taking time because there's some related work across the operators in k8ssandra that go with this.

Solution patch:
diff --git a/docs/azure_blobs_setup.md b/docs/azure_blobs_setup.md
index 447dbc7..afd3b3f 100644
--- a/docs/azure_blobs_setup.md
+++ b/docs/azure_blobs_setup.md
@@ -11,7 +11,35 @@ Create a new storage account or use an existing one which will be used to store
     "key": "YOUR_KEY"
 }
 ```
-Place this file on all Apache Cassandra™ nodes running medusa under `/etc/medusa/`and set the rights appropriately so that only users running Medusa can read/modify it.
+Place this file on all Apache Cassandra™ nodes running Medusa under `/etc/medusa/`and set the rights appropriately so that only users running Medusa can read/modify it.
+
+
+### Identity through roles
+
+If Medusa runs in an environment with a managed identity set up, then there is no need to create the key file, nor to put the `key_file` field in the `medusa.ini` file.
+
+You can check if the managed identity works by running:
+
+```
+az login --identity
+az storage blob list  --account-name <account_name> --container-name <container_name> --auth-mode login --output table
+```
+
+  * The `--account-name` is the name of the Azure account where the storage is located.
+  * The `--container-name` is the name of the container where the blobs are store. This is equivalent to the _bucket name_ in other storage providers.
+
+To have Medusa use the managed identity, you need to set the `AZURE_STORAGE_ACCOUNT` environment variable before you run `medusa`:
+
+```
+export AZURE_STORAGE_ACCOUNT=<account_name>
+```
+
+In the `medusa.ini` file, set only the `bucket_name` to your container name. You should see log lines like these if everything is working as expected:
+
+```
+[2025-03-24 15:47:22,191] INFO: ManagedIdentityCredential will use IMDS
+[2025-03-24 15:47:22,215] INFO: DefaultAzureCredential acquired a token from ManagedIdentityCredential
+```
 
 ### Create a container
 
diff --git a/docs/gcs_setup.md b/docs/gcs_setup.md
index 6f2be38..fabd802 100644
--- a/docs/gcs_setup.md
+++ b/docs/gcs_setup.md
@@ -35,7 +35,7 @@ gsutil mb -p ${GCP_PROJECT} -c regional -l ${LOCATION} ${BUCKET_URL}
 
 ### Create a service account and download its keys
 
-Medusa will require a `credentials.json` file with the information and keys for a service account with the appropriate role in order to interact with the bucket.
+Medusa will require a `credentials.json` file with the information and keys for a service account with the appropriate role in order to interact with the bucket.
 
 Create the service account (if it doesn't exist yet):
 
@@ -43,7 +43,7 @@ Create the service account (if it doesn't exist yet):
 gcloud --project ${GCP_PROJECT} iam service-accounts create ${SERVICE_ACCOUNT_NAME} --display-name ${SERVICE_ACCOUNT_NAME}
 ```
 
-### Configure the service account with the role
+### Configure the service account with the role
 
 Once the service account has been created, and considering [jq](https://stedolan.github.io/jq/) is installed, run the following command to add the `MedusaStorageRole` to it, for our backup bucket:
 
diff --git a/medusa/storage/azure_storage.py b/medusa/storage/azure_storage.py
index 3662b6e..84dd70a 100644
--- a/medusa/storage/azure_storage.py
+++ b/medusa/storage/azure_storage.py
@@ -24,6 +24,7 @@ import pathlib
 import typing as t
 
 from azure.core.credentials import AzureNamedKeyCredential
+from azure.identity import DefaultAzureCredential
 from azure.storage.blob.aio import BlobServiceClient
 from azure.storage.blob import BlobProperties, StandardBlobTier
 from medusa.storage.abstract_storage import AbstractStorage, AbstractBlob, AbstractBlobMetadata, ObjectDoesNotExistError
@@ -40,14 +41,23 @@ class AzureStorage(AbstractStorage):
 
     def __init__(self, config):
 
-        credentials_file = Path(config.key_file).expanduser()
-        with open(credentials_file, "r") as f:
-            credentials_dict = json.loads(f.read())
-            self.credentials = AzureNamedKeyCredential(
-                name=credentials_dict["storage_account"],
-                key=credentials_dict["key"]
-            )
-        self.account_name = self.credentials.named_key.name
+        if config.key_file is not None:
+            credentials_file = Path(config.key_file).expanduser()
+            logging.debug(f"Loading identity from credentials file: {credentials_file.absolute()}")
+            with open(credentials_file, "r") as f:
+                credentials_dict = json.loads(f.read())
+                self.credentials = AzureNamedKeyCredential(
+                    name=credentials_dict["storage_account"],
+                    key=credentials_dict["key"]
+                )
+                self.account_name = self.credentials.named_key.name
+        else:
+            logging.debug("No credentials file specified, using DefaultAzureCredential")
+            self.credentials = DefaultAzureCredential()
+            self.account_name = os.environ.get('AZURE_STORAGE_ACCOUNT', None)
+            if self.account_name is None:
+                raise ValueError("No Azure storage account name specified in AZURE_STORAGE_ACCOUNT env variable")
+
         self.bucket_name = config.bucket_name
 
         self.azure_blob_service_url = self._make_blob_service_url(self.account_name, config)
diff --git a/medusa/storage/google_storage.py b/medusa/storage/google_storage.py
index 18f0e90..39a63ce 100644
--- a/medusa/storage/google_storage.py
+++ b/medusa/storage/google_storage.py
@@ -41,10 +41,10 @@ class GoogleStorage(AbstractStorage):
     def __init__(self, config):
         if config.key_file is not None:
             self.service_file = str(Path(config.key_file).expanduser())
-            logging.info("Using service file: {}".format(self.service_file))
+            logging.debug("Using service file: {}".format(self.service_file))
         else:
             self.service_file = None
-            logging.info("Using attached service account")
+            logging.debug("Using attached service account")
 
         self.bucket_name = config.bucket_name
 


Test patch:
diff --git a/tests/storage/azure_storage_test.py b/tests/storage/azure_storage_test.py
index 02467a5..70ec749 100644
--- a/tests/storage/azure_storage_test.py
+++ b/tests/storage/azure_storage_test.py
@@ -5,9 +5,13 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import os
 import tempfile
 import unittest
 
+from azure.core.credentials import AzureNamedKeyCredential
+from azure.identity import DefaultAzureCredential
+
 from medusa.storage.azure_storage import AzureStorage
 from tests.storage.abstract_storage_test import AttributeDict
 
@@ -36,6 +40,7 @@ class AzureStorageTest(unittest.TestCase):
                 'read_timeout': 60,
             })
             azure_storage = AzureStorage(config)
+            self.assertTrue(isinstance(azure_storage.credentials, AzureNamedKeyCredential))
             self.assertEqual(
                 'https://medusa-unit-test.blob.core.windows.net/',
                 azure_storage.azure_blob_service_url
@@ -56,6 +61,7 @@ class AzureStorageTest(unittest.TestCase):
                 'read_timeout': 60,
             })
             azure_storage = AzureStorage(config)
+            self.assertTrue(isinstance(azure_storage.credentials, AzureNamedKeyCredential))
             self.assertEqual(
                 'https://medusa-unit-test.blob.core.custom.host.net/',
                 azure_storage.azure_blob_service_url
@@ -76,7 +82,28 @@ class AzureStorageTest(unittest.TestCase):
                 'read_timeout': 60,
             })
             azure_storage = AzureStorage(config)
+            self.assertTrue(isinstance(azure_storage.credentials, AzureNamedKeyCredential))
             self.assertEqual(
                 'https://medusa-unit-test.blob.core.custom.host.net:123/',
                 azure_storage.azure_blob_service_url
             )
+
+    def test_use_default_azure_credentials(self):
+        config = AttributeDict({
+            'region': 'region-from-config',
+            'storage_provider': 'azure_blobs',
+            'bucket_name': 'bucket-from-config',
+            'concurrent_transfers': '1',
+            'host': None,
+            'port': None,
+            'read_timeout': 60,
+            'key_file': None,
+        })
+        os.environ['AZURE_STORAGE_ACCOUNT'] = 'testAccount'
+        azure_storage = AzureStorage(config)
+        self.assertTrue(isinstance(azure_storage.credentials, DefaultAzureCredential))
+        # we need the account name for making the connection url
+        self.assertEqual(
+            'https://testAccount.blob.core.windows.net/',
+            azure_storage.azure_blob_service_url
+        )