summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/sources/DataSourceMAAS.py54
-rw-r--r--tests/unittests/test_datasource/test_maas.py53
2 files changed, 86 insertions, 21 deletions
diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py
index 496bd06a..6ac88635 100644
--- a/cloudinit/sources/DataSourceMAAS.py
+++ b/cloudinit/sources/DataSourceMAAS.py
@@ -8,6 +8,7 @@
from __future__ import print_function
+import hashlib
import os
import time
@@ -41,25 +42,20 @@ class DataSourceMAAS(sources.DataSource):
"""
dsname = "MAAS"
+ id_hash = None
+ _oauth_helper = None
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.base_url = None
self.seed_dir = os.path.join(paths.seed_dir, 'maas')
- self.oauth_helper = self._get_helper()
+ self.id_hash = get_id_from_ds_cfg(self.ds_cfg)
- def _get_helper(self):
- mcfg = self.ds_cfg
- # If we are missing token_key, token_secret or consumer_key
- # then just do non-authed requests
- for required in ('token_key', 'token_secret', 'consumer_key'):
- if required not in mcfg:
- return url_helper.OauthUrlHelper()
-
- return url_helper.OauthUrlHelper(
- consumer_key=mcfg['consumer_key'], token_key=mcfg['token_key'],
- token_secret=mcfg['token_secret'],
- consumer_secret=mcfg.get('consumer_secret'))
+ @property
+ def oauth_helper(self):
+ if not self._oauth_helper:
+ self._oauth_helper = get_oauth_helper(self.ds_cfg)
+ return self._oauth_helper
def __str__(self):
root = sources.DataSource.__str__(self)
@@ -147,6 +143,36 @@ class DataSourceMAAS(sources.DataSource):
return bool(url)
+ def check_instance_id(self, sys_cfg):
+ """locally check if the current system is the same instance.
+
+ MAAS doesn't provide a real instance-id, and if it did, it is
+ still only available over the network. We need to check based
+ only on local resources. So compute a hash based on Oauth tokens."""
+ if self.id_hash is None:
+ return False
+ ncfg = util.get_cfg_by_path(sys_cfg, ("datasource", self.dsname), {})
+ return (self.id_hash == get_id_from_ds_cfg(ncfg))
+
+
+def get_oauth_helper(cfg):
+ """Return an oauth helper instance for values in cfg.
+
+ @raises ValueError from OauthUrlHelper if some required fields have
+ true-ish values but others do not."""
+ keys = ('consumer_key', 'consumer_secret', 'token_key', 'token_secret')
+ kwargs = dict([(r, cfg.get(r)) for r in keys])
+ return url_helper.OauthUrlHelper(**kwargs)
+
+
+def get_id_from_ds_cfg(ds_cfg):
+ """Given a config, generate a unique identifier for this node."""
+ fields = ('consumer_key', 'token_key', 'token_secret')
+ idstr = '\0'.join([ds_cfg.get(f, "") for f in fields])
+ # store the encoding version as part of the hash in the event
+ # that it ever changed we can compute older versions.
+ return 'v1:' + hashlib.sha256(idstr.encode('utf-8')).hexdigest()
+
def read_maas_seed_dir(seed_d):
if seed_d.startswith("file://"):
@@ -322,7 +348,7 @@ if __name__ == "__main__":
sys.stderr.write("Must provide a url or a config with url.\n")
sys.exit(1)
- oauth_helper = url_helper.OauthUrlHelper(**creds)
+ oauth_helper = get_oauth_helper(creds)
def geturl(url):
# the retry is to ensure that oauth timestamp gets fixed
diff --git a/tests/unittests/test_datasource/test_maas.py b/tests/unittests/test_datasource/test_maas.py
index 289c6a40..6e4031cf 100644
--- a/tests/unittests/test_datasource/test_maas.py
+++ b/tests/unittests/test_datasource/test_maas.py
@@ -1,6 +1,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
from copy import copy
+import mock
import os
import shutil
import tempfile
@@ -8,15 +9,10 @@ import yaml
from cloudinit.sources import DataSourceMAAS
from cloudinit import url_helper
-from cloudinit.tests.helpers import TestCase, populate_dir
+from cloudinit.tests.helpers import CiTestCase, populate_dir
-try:
- from unittest import mock
-except ImportError:
- import mock
-
-class TestMAASDataSource(TestCase):
+class TestMAASDataSource(CiTestCase):
def setUp(self):
super(TestMAASDataSource, self).setUp()
@@ -159,4 +155,47 @@ class TestMAASDataSource(TestCase):
self.assertEqual(valid['meta-data/instance-id'], md['instance-id'])
self.assertEqual(expected_vd, vd)
+
+@mock.patch("cloudinit.sources.DataSourceMAAS.url_helper.OauthUrlHelper")
+class TestGetOauthHelper(CiTestCase):
+ with_logs = True
+ base_cfg = {'consumer_key': 'FAKE_CONSUMER_KEY',
+ 'token_key': 'FAKE_TOKEN_KEY',
+ 'token_secret': 'FAKE_TOKEN_SECRET',
+ 'consumer_secret': None}
+
+ def test_all_required(self, m_helper):
+ """Valid config as expected."""
+ DataSourceMAAS.get_oauth_helper(self.base_cfg.copy())
+ m_helper.assert_has_calls([mock.call(**self.base_cfg)])
+
+ def test_other_fields_not_passed_through(self, m_helper):
+ """Only relevant fields are passed through."""
+ mycfg = self.base_cfg.copy()
+ mycfg['unrelated_field'] = 'unrelated'
+ DataSourceMAAS.get_oauth_helper(mycfg)
+ m_helper.assert_has_calls([mock.call(**self.base_cfg)])
+
+
+class TestGetIdHash(CiTestCase):
+ v1_cfg = {'consumer_key': 'CKEY', 'token_key': 'TKEY',
+ 'token_secret': 'TSEC'}
+ v1_id = (
+ 'v1:'
+ '403ee5f19c956507f1d0e50814119c405902137ea4f8838bde167c5da8110392')
+
+ def test_v1_expected(self):
+ """Test v1 id generated as expected working behavior from config."""
+ result = DataSourceMAAS.get_id_from_ds_cfg(self.v1_cfg.copy())
+ self.assertEqual(self.v1_id, result)
+
+ def test_v1_extra_fields_are_ignored(self):
+ """Test v1 id ignores unused entries in config."""
+ cfg = self.v1_cfg.copy()
+ cfg['consumer_secret'] = "BOO"
+ cfg['unrelated'] = "HI MOM"
+ result = DataSourceMAAS.get_id_from_ds_cfg(cfg)
+ self.assertEqual(self.v1_id, result)
+
+
# vi: ts=4 expandtab