Repository: canonical/operator
Pull request #563 (2021-07-08 14:46:51)

Expose model uuid in ops.model.Model
In LMA, we need access to the Model UUID to be able to tag telemetry in a way that allows us to consistently deduplicate it based on the Juju topology (model name, model uuid, application name, unit id).

This PR exposes `uuid` as a property of `ops.model.Model` so that its value can be used to tag telemetry.


Patch:
diff --git a/ops/model.py b/ops/model.py
index d10c3f0..219d992 100644
--- a/ops/model.py
+++ b/ops/model.py
@@ -106,6 +106,14 @@ class Model:
         """
         return self._backend.model_name
 
+    @property
+    def uuid(self) -> str:
+        """Return the identifier of the Model that this unit is running in.
+
+        This is read from the environment variable ``JUJU_MODEL_UUID``.
+        """
+        return self._backend.model_uuid
+
     def get_unit(self, unit_name: str) -> 'Unit':
         """Get an arbitrary unit by name.
 
@@ -1272,14 +1280,17 @@ class _ModelBackend:
 
     LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30)
 
-    def __init__(self, unit_name=None, model_name=None):
+    def __init__(self, unit_name=None, model_name=None, model_uuid=None):
         if unit_name is None:
             self.unit_name = os.environ['JUJU_UNIT_NAME']
         else:
             self.unit_name = unit_name
         if model_name is None:
             model_name = os.environ.get('JUJU_MODEL_NAME')
+        if model_uuid is None:
+            model_uuid = os.environ.get('JUJU_MODEL_UUID')
         self.model_name = model_name
+        self.model_uuid = model_uuid
         self.app_name = self.unit_name.split('/')[0]
 
         self._is_leader = None


Test patch:
diff --git a/ops/testing.py b/ops/testing.py
index 5a9ed27..50d1438 100755
--- a/ops/testing.py
+++ b/ops/testing.py
@@ -521,6 +521,18 @@ class Harness(typing.Generic[CharmType]):
         """Read the workload version that was set by the unit."""
         return self._backend._workload_version
 
+    def set_model_info(self, name: str = None, uuid: str = None) -> None:
+        """Set the name and uuid of the Model that this is representing.
+
+        This cannot be called once begin() has been called. But it lets you set the value that
+        will be returned by Model.name and Model.uuid.
+
+        This is a convenience method to invoke both Harness.set_model_name
+        and Harness.set_model_uuid at once.
+        """
+        self.set_model_name(name)
+        self.set_model_uuid(uuid)
+
     def set_model_name(self, name: str) -> None:
         """Set the name of the Model that this is representing.
 
@@ -531,6 +543,16 @@ class Harness(typing.Generic[CharmType]):
             raise RuntimeError('cannot set the Model name after begin()')
         self._backend.model_name = name
 
+    def set_model_uuid(self, uuid: str) -> None:
+        """Set the uuid of the Model that this is representing.
+
+        This cannot be called once begin() has been called. But it lets you set the value that
+        will be returned by Model.uuid.
+        """
+        if self._charm is not None:
+            raise RuntimeError('cannot set the Model uuid after begin()')
+        self._backend.model_uuid = uuid
+
     def update_relation_data(
             self,
             relation_id: int,
@@ -746,6 +768,7 @@ class _TestingModelBackend:
         self.unit_name = unit_name
         self.app_name = self.unit_name.split('/')[0]
         self.model_name = None
+        self.model_uuid = 'f2c1b2a6-e006-11eb-ba80-0242ac130004'
         self._calls = []
         self._meta = meta
         self._is_leader = None
diff --git a/test/test_testing.py b/test/test_testing.py
index 3080d33..179d249 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -670,6 +670,38 @@ class TestHarness(unittest.TestCase):
             harness.set_model_name('foo')
         self.assertEqual(harness.model.name, 'bar')
 
+    def test_set_model_uuid_after_begin(self):
+        harness = Harness(CharmBase, meta='''
+            name: test-charm
+        ''')
+        self.addCleanup(harness.cleanup)
+        harness.set_model_name('bar')
+        harness.set_model_uuid('96957e90-e006-11eb-ba80-0242ac130004')
+        harness.begin()
+        with self.assertRaises(RuntimeError):
+            harness.set_model_uuid('af0479ea-e006-11eb-ba80-0242ac130004')
+        self.assertEqual(harness.model.uuid, '96957e90-e006-11eb-ba80-0242ac130004')
+
+    def test_set_model_info_after_begin(self):
+        harness = Harness(CharmBase, meta='''
+            name: test-charm
+        ''')
+        self.addCleanup(harness.cleanup)
+        harness.set_model_info('foo', '96957e90-e006-11eb-ba80-0242ac130004')
+        harness.begin()
+        with self.assertRaises(RuntimeError):
+            harness.set_model_info('bar', 'af0479ea-e006-11eb-ba80-0242ac130004')
+        with self.assertRaises(RuntimeError):
+            harness.set_model_info('bar')
+        with self.assertRaises(RuntimeError):
+            harness.set_model_info(uuid='af0479ea-e006-11eb-ba80-0242ac130004')
+        with self.assertRaises(RuntimeError):
+            harness.set_model_name('bar')
+        with self.assertRaises(RuntimeError):
+            harness.set_model_uuid('af0479ea-e006-11eb-ba80-0242ac130004')
+        self.assertEqual(harness.model.name, 'foo')
+        self.assertEqual(harness.model.uuid, '96957e90-e006-11eb-ba80-0242ac130004')
+
     def test_actions_from_directory(self):
         tmp = pathlib.Path(tempfile.mkdtemp())
         self.addCleanup(shutil.rmtree, str(tmp))