Merge ~smoser/cloud-init:bug/1712680-maas-use-token-for-check-instance-id into cloud-init:master

Proposed by Scott Moser on 2017-12-12
Status: Needs review
Proposed branch: ~smoser/cloud-init:bug/1712680-maas-use-token-for-check-instance-id
Merge into: cloud-init:master
Diff against target: 170 lines (+86/-21)
2 files modified
cloudinit/sources/DataSourceMAAS.py (+40/-14)
tests/unittests/test_datasource/test_maas.py (+46/-7)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve 6 hours ago
Chad Smith Approve 6 hours ago
Ryan Harper 2017-12-12 Needs Fixing on 2017-12-13
Review via email: mp+335108@code.launchpad.net

Commit Message

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

To post a comment you must log in.
Scott Moser (smoser) wrote :

no tests, but I'm interested in thoughts.
We could make 'id_hash' a property since ds_cfg gets stored.

Chad I'm interested in your thoughts on this and the unification of storage, how did I affect that?

PASSED: Continuous integration, rev:cefcd130bfb452453fbfd56c83dd175de258c620
https://jenkins.ubuntu.com/server/job/cloud-init-ci/624/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/624/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) wrote :

I know this is a completely separate direction than you are taking.

It feels a bit like we are gating all cloud-init updates/runs based on whether the instance-id changes. In a world where cloud-init becomes re-entrany (or hotplug handling) should cloud-init grow the ability for people to supplement cloud-init's written content, with more information?

I'm thinking like bookend comments like this.
# begin cloud-init ignore
# end cloud-init ignore

In either case, I'll try deploying a node twice with reboots in between and make sure this only changes on node re-install. It feels like we are working around a problem by relying on instance-id as our gate for rewriting content. Should cloud-init ever accept

Chad Smith (chad.smith) wrote :

Another question I have is across reboots (same instance) do we worry about MAAS changing network topology between boots? If so, locking network setup permanently until re-deploy would cause breakage

PASSED: Continuous integration, rev:2543c765cba201570f697ccf5d5efc5a75b65322
https://jenkins.ubuntu.com/server/job/cloud-init-ci/625/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/625/rebuild

review: Approve (continuous-integration)
Ryan Harper (raharper) wrote :

Looking at a deployed node, the instance id is node-<uuid>;

IIUC, upon reboot, the MAAS Datasource url is re-crawled, and in that metadata url, it has an instance-id; and if that matches the 'previous-iid' that's on disk, then we assume it's a reboot of the same instance.

Then, instead of MAAS including the instance-id (node-uuid) bit in the original configuration that's written out to /etc/cloud/cloud.cfg.d/90-dpkg_maas.cfg or where-ever) we're combining the elements of the configuration into a hash.

That's pretty neat.

--
Now, w.r.t the code. My first worry was about upgrade. Since we always throw away the obj.pkl until this version; I don't think we'll have to worry about loading an object that doesn't have those internal members; however if someone were to "manually run cloud-init init" again, then I think we have that same issue here. We should handle that just because we're aware of that.

review: Needs Fixing
Ryan Harper (raharper) wrote :

> I know this is a completely separate direction than you are taking.
>
> It feels a bit like we are gating all cloud-init updates/runs based on whether
> the instance-id changes. In a world where cloud-init becomes re-entrany (or
> hotplug handling) should cloud-init grow the ability for people to supplement
> cloud-init's written content, with more information?
>
> I'm thinking like bookend comments like this.
> # begin cloud-init ignore
> # end cloud-init ignore

I'm not sure a book-end sort of comment is going to be sufficient; generally
there are config management engines that handle this sort of thing better.
However, I do think we can look toward creating idempotent config modules which
can check if they need to do anything w.r.t updates to files, versus blind rendering.

>
> In either case, I'll try deploying a node twice with reboots in between and
> make sure this only changes on node re-install. It feels like we are working
> around a problem by relying on instance-id as our gate for rewriting content.
> Should cloud-init ever accept

I think that's generally the design of instance-id; I don't think that's an issue.
In the event we want to react to changes in user-data/metadata then I think the
code that renders based on the metadata will need to handle that it may be called
many times, rather than once-per instance.

Ryan Harper (raharper) wrote :

> Another question I have is across reboots (same instance) do we worry about
> MAAS changing network topology between boots? If so, locking network setup
> permanently until re-deploy would cause breakage

MAAS , AFAIK, does not change/update metadata (including network configuration) between
deployments of a node. The node is allocated and deployed and remains the same (from a metadata perspective) until the node is released from MAAS for a new deployment.

Scott Moser (smoser) wrote :

@Chad, @Ryan
We do need to have way to allow user updates to a file without blowing
them away. This is non-trivial for sure.

That said, rendering some things that cloud-init "owns" on first instance
is necessary. If the user had made edits to files on an old instance,
with possibly instance-specific data inside it then it is quite likely
that those modifications are not consistent with the new instance. One
solution we had to this (/etc/hosts, /etc/apt/sources.list) is to render
the files from a template, and to allow the user to modify the template
source then if they want to make changes that persist across an instance,
they change the template.

network config wasn't really ever meant to be "multi-party updatable", so
this is non-trivial in this case. netplan has some sort of plan for that
(multiple .yaml files in /etc/netplan) but the reality is that it will
still be non-trivial. Multi-party modification of *anything* is non-
trivial.

@Ryan,
I'm not sure weather or not this hits the attribute missing-on-second-run
bug that we saw in bug 1732917. I'll check to make sure.

Over all, this is a simple-ish fix that does dramatically improve the
state of maas reboot scenario. It does come at the cost of now
requiring the user to "clean" the image if they want to take the image
out of MAAS, but that isn't actually a reality at this point that I'm
aware of.

Scott Moser (smoser) wrote :

I played a bit and I'm pretty sure this is safe
Heres an example which can be built on,b ut shows store and reload with changing class.

  http://paste.ubuntu.com/26177197/

Ryan Harper (raharper) wrote :

After discussion, the use of self.hash_id is safe because we don't expect a non-none value as we did in other cases of restored object code.

While discussing this in IRC, discovered that self.hash_id isn't set in get_data()
That needs doing and unittests.

Otherwise, it looks good.

13e000d... by Scott Moser on 2017-12-13

actually store the original value

3a3d6fc... by Scott Moser on 2017-12-21

address chad's feedback

Scott Moser (smoser) :

FAILED: Continuous integration, rev:ed4c0c24c1f19140432180a22e7069e2ee49f9ba
https://jenkins.ubuntu.com/server/job/cloud-init-ci/653/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/653/rebuild

review: Needs Fixing (continuous-integration)
886ad31... by Scott Moser on 2018-01-08

fix issues raised by c-i review, add some tests.

PASSED: Continuous integration, rev:7cef59571b99857ddb5241c5d8b4f3a982423763
https://jenkins.ubuntu.com/server/job/cloud-init-ci/671/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/671/rebuild

review: Approve (continuous-integration)
c726f64... by Scott Moser on 2018-01-08

add tests of v1 hash

PASSED: Continuous integration, rev:f55dd0e0f67b1dc7fbd92c4cea7b06e312111139
https://jenkins.ubuntu.com/server/job/cloud-init-ci/672/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/672/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) :
review: Needs Information
Scott Moser (smoser) :
b5264bd... by Scott Moser 11 hours ago

remove warning / dropping of oauth

previously if the user did not provide all required fields,
the 'get_oauth_helper' would just drop to non-oauth (and warn).
that just seems odd. now instead, that will go through to the
OauthHelper constructor and raise a ValueError.

FAILED: Continuous integration, rev:cefe421e2ed7b4fcb52f10e0ce1b4d3a8c6ee1bd
https://jenkins.ubuntu.com/server/job/cloud-init-ci/681/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/681/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:b5264bdb06669f956f6cc4fd719e38aefc6310b8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/682/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/682/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) wrote :

+1 just a minor docstr fix request

Chad Smith (chad.smith) :
review: Approve
ae66049... by Scott Moser 6 hours ago

fix docstring

PASSED: Continuous integration, rev:ae66049ceee919e364b1054cdd782e41e9b68f44
https://jenkins.ubuntu.com/server/job/cloud-init-ci/684/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/684/rebuild

review: Approve (continuous-integration)

Unmerged commits

ae66049... by Scott Moser 6 hours ago

fix docstring

b5264bd... by Scott Moser 11 hours ago

remove warning / dropping of oauth

previously if the user did not provide all required fields,
the 'get_oauth_helper' would just drop to non-oauth (and warn).
that just seems odd. now instead, that will go through to the
OauthHelper constructor and raise a ValueError.

c726f64... by Scott Moser on 2018-01-08

add tests of v1 hash

886ad31... by Scott Moser on 2018-01-08

fix issues raised by c-i review, add some tests.

3a3d6fc... by Scott Moser on 2017-12-21

address chad's feedback

13e000d... by Scott Moser on 2017-12-13

actually store the original value

d47aedd... by Scott Moser on 2017-12-12

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py
2index 496bd06..6ac8863 100644
3--- a/cloudinit/sources/DataSourceMAAS.py
4+++ b/cloudinit/sources/DataSourceMAAS.py
5@@ -8,6 +8,7 @@
6
7 from __future__ import print_function
8
9+import hashlib
10 import os
11 import time
12
13@@ -41,25 +42,20 @@ class DataSourceMAAS(sources.DataSource):
14 """
15
16 dsname = "MAAS"
17+ id_hash = None
18+ _oauth_helper = None
19
20 def __init__(self, sys_cfg, distro, paths):
21 sources.DataSource.__init__(self, sys_cfg, distro, paths)
22 self.base_url = None
23 self.seed_dir = os.path.join(paths.seed_dir, 'maas')
24- self.oauth_helper = self._get_helper()
25+ self.id_hash = get_id_from_ds_cfg(self.ds_cfg)
26
27- def _get_helper(self):
28- mcfg = self.ds_cfg
29- # If we are missing token_key, token_secret or consumer_key
30- # then just do non-authed requests
31- for required in ('token_key', 'token_secret', 'consumer_key'):
32- if required not in mcfg:
33- return url_helper.OauthUrlHelper()
34-
35- return url_helper.OauthUrlHelper(
36- consumer_key=mcfg['consumer_key'], token_key=mcfg['token_key'],
37- token_secret=mcfg['token_secret'],
38- consumer_secret=mcfg.get('consumer_secret'))
39+ @property
40+ def oauth_helper(self):
41+ if not self._oauth_helper:
42+ self._oauth_helper = get_oauth_helper(self.ds_cfg)
43+ return self._oauth_helper
44
45 def __str__(self):
46 root = sources.DataSource.__str__(self)
47@@ -147,6 +143,36 @@ class DataSourceMAAS(sources.DataSource):
48
49 return bool(url)
50
51+ def check_instance_id(self, sys_cfg):
52+ """locally check if the current system is the same instance.
53+
54+ MAAS doesn't provide a real instance-id, and if it did, it is
55+ still only available over the network. We need to check based
56+ only on local resources. So compute a hash based on Oauth tokens."""
57+ if self.id_hash is None:
58+ return False
59+ ncfg = util.get_cfg_by_path(sys_cfg, ("datasource", self.dsname), {})
60+ return (self.id_hash == get_id_from_ds_cfg(ncfg))
61+
62+
63+def get_oauth_helper(cfg):
64+ """Return an oauth helper instance for values in cfg.
65+
66+ @raises ValueError from OauthUrlHelper if some required fields have
67+ true-ish values but others do not."""
68+ keys = ('consumer_key', 'consumer_secret', 'token_key', 'token_secret')
69+ kwargs = dict([(r, cfg.get(r)) for r in keys])
70+ return url_helper.OauthUrlHelper(**kwargs)
71+
72+
73+def get_id_from_ds_cfg(ds_cfg):
74+ """Given a config, generate a unique identifier for this node."""
75+ fields = ('consumer_key', 'token_key', 'token_secret')
76+ idstr = '\0'.join([ds_cfg.get(f, "") for f in fields])
77+ # store the encoding version as part of the hash in the event
78+ # that it ever changed we can compute older versions.
79+ return 'v1:' + hashlib.sha256(idstr.encode('utf-8')).hexdigest()
80+
81
82 def read_maas_seed_dir(seed_d):
83 if seed_d.startswith("file://"):
84@@ -322,7 +348,7 @@ if __name__ == "__main__":
85 sys.stderr.write("Must provide a url or a config with url.\n")
86 sys.exit(1)
87
88- oauth_helper = url_helper.OauthUrlHelper(**creds)
89+ oauth_helper = get_oauth_helper(creds)
90
91 def geturl(url):
92 # the retry is to ensure that oauth timestamp gets fixed
93diff --git a/tests/unittests/test_datasource/test_maas.py b/tests/unittests/test_datasource/test_maas.py
94index 289c6a4..6e4031c 100644
95--- a/tests/unittests/test_datasource/test_maas.py
96+++ b/tests/unittests/test_datasource/test_maas.py
97@@ -1,6 +1,7 @@
98 # This file is part of cloud-init. See LICENSE file for license information.
99
100 from copy import copy
101+import mock
102 import os
103 import shutil
104 import tempfile
105@@ -8,15 +9,10 @@ import yaml
106
107 from cloudinit.sources import DataSourceMAAS
108 from cloudinit import url_helper
109-from cloudinit.tests.helpers import TestCase, populate_dir
110+from cloudinit.tests.helpers import CiTestCase, populate_dir
111
112-try:
113- from unittest import mock
114-except ImportError:
115- import mock
116
117-
118-class TestMAASDataSource(TestCase):
119+class TestMAASDataSource(CiTestCase):
120
121 def setUp(self):
122 super(TestMAASDataSource, self).setUp()
123@@ -159,4 +155,47 @@ class TestMAASDataSource(TestCase):
124 self.assertEqual(valid['meta-data/instance-id'], md['instance-id'])
125 self.assertEqual(expected_vd, vd)
126
127+
128+@mock.patch("cloudinit.sources.DataSourceMAAS.url_helper.OauthUrlHelper")
129+class TestGetOauthHelper(CiTestCase):
130+ with_logs = True
131+ base_cfg = {'consumer_key': 'FAKE_CONSUMER_KEY',
132+ 'token_key': 'FAKE_TOKEN_KEY',
133+ 'token_secret': 'FAKE_TOKEN_SECRET',
134+ 'consumer_secret': None}
135+
136+ def test_all_required(self, m_helper):
137+ """Valid config as expected."""
138+ DataSourceMAAS.get_oauth_helper(self.base_cfg.copy())
139+ m_helper.assert_has_calls([mock.call(**self.base_cfg)])
140+
141+ def test_other_fields_not_passed_through(self, m_helper):
142+ """Only relevant fields are passed through."""
143+ mycfg = self.base_cfg.copy()
144+ mycfg['unrelated_field'] = 'unrelated'
145+ DataSourceMAAS.get_oauth_helper(mycfg)
146+ m_helper.assert_has_calls([mock.call(**self.base_cfg)])
147+
148+
149+class TestGetIdHash(CiTestCase):
150+ v1_cfg = {'consumer_key': 'CKEY', 'token_key': 'TKEY',
151+ 'token_secret': 'TSEC'}
152+ v1_id = (
153+ 'v1:'
154+ '403ee5f19c956507f1d0e50814119c405902137ea4f8838bde167c5da8110392')
155+
156+ def test_v1_expected(self):
157+ """Test v1 id generated as expected working behavior from config."""
158+ result = DataSourceMAAS.get_id_from_ds_cfg(self.v1_cfg.copy())
159+ self.assertEqual(self.v1_id, result)
160+
161+ def test_v1_extra_fields_are_ignored(self):
162+ """Test v1 id ignores unused entries in config."""
163+ cfg = self.v1_cfg.copy()
164+ cfg['consumer_secret'] = "BOO"
165+ cfg['unrelated'] = "HI MOM"
166+ result = DataSourceMAAS.get_id_from_ds_cfg(cfg)
167+ self.assertEqual(self.v1_id, result)
168+
169+
170 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches

to all changes: