summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2018-01-10 13:53:17 -0700
committerChad Smith <blackboxsw@gmail.com>2018-01-10 13:53:17 -0700
commit5f550420d2ed9d9ef024293f33d33f0f2fc04ee5 (patch)
treeb34f293f22fa2dec14c348e8a8580df93e670382
parentdf24daa833d7eb88e7c172eb5d7f257766adb0e3 (diff)
downloadvyos-cloud-init-5f550420d2ed9d9ef024293f33d33f0f2fc04ee5.tar.gz
vyos-cloud-init-5f550420d2ed9d9ef024293f33d33f0f2fc04ee5.zip
MAAS: add check_instance_id based off oauth tokens.
This stores a hash of the OAuth tokens as an 'id' for the maas datasource. Since new instances get new tokens created and those tokens are written by curtin into datasource system config this will provide a way to identify a new "instance" (install). LP: #1712680
-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