Merge ~chad.smith/cloud-init:feature/maintain-network-on-boot into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Chad Smith
Approved revision: 61355a62829261b251f9741bced741483a4ce53c
Merge reported by: Chad Smith
Merged at revision: be9ecc12823607b4709b64408aee137bfdfc7d01
Proposed branch: ~chad.smith/cloud-init:feature/maintain-network-on-boot
Merge into: cloud-init:master
Diff against target: 522 lines (+417/-6)
5 files modified
cloudinit/event.py (+17/-0)
cloudinit/sources/__init__.py (+77/-1)
cloudinit/sources/tests/test_init.py (+82/-1)
cloudinit/stages.py (+10/-4)
cloudinit/tests/test_stages.py (+231/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Approve
Review via email: mp+348000@code.launchpad.net

Commit message

update_metadata: a datasource can support network re-config every boot

Very basic type definitions are now defined to distinguish 'boot'
events from 'new instance (first boot)'. Event types will now be handed
to a datasource.update_metadata method which can determine whether
to refresh its metadata and re-render configuration based on that
source event.

A datasource can 'subscribe' to an event by setting up the update_events
attribute on the datasource class which describe what config scope is
updated by a list of matching events. By default datasources will have
the following update_events: {'network': [EventType.BOOT_NEW_INSTANCE]}
This setting says the datasource will re-write network configuration only
on first boot of a new instance or when the instance id changes.

New methods are now present on the datasource:
  - clear_cached_attrs: Resets cached datasource attributes to values
    listed in datasource.cached_attr_defaults. This is performed prior to
    processing a fresh metadata process to avoid keeping old/invalid
    cached data around.
  - update_metadata: accepts source_event_types to determine if the
    metadata should be crawled again and processed

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

so where would the logic of "apply if changed" be placed?

Revision history for this message
Ryan Harper (raharper) wrote :

I'm not super happy with the Maintenance name. Maybe SystemEvent? Also, I think we need other classes of Event types; DatasourceEvents (METADATA_REFRESHED) or PlatformEvents; Maybe we'll be notified of a pending instance migration, pending shutdown, etc.

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Mike Gerdts (mgerdts) wrote :

This seems to meet my needs for lp#1765801

Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Mike Gerdts (mgerdts) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
82ae6c7... by Chad Smith

pylint

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
45772ff... by Chad Smith

increment count in unit tests

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:45772ffa3f32ac200d182078674af3b5b76af719
https://jenkins.ubuntu.com/server/job/cloud-init-ci/127/
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/127/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
4efecd0... by Chad Smith

scope update_events to specific config keys, such as network

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
959cb0c... by Chad Smith

rework logic of clear cache add unit test for clearing _network_config

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

> so where would the logic of "apply if changed" be placed?

In the latest iteration of this branch our "apply if change logic" will require some followup work.

I think we need a branch that will decompose get_data into the following separate methods:
 1. ds.detect() - speedy/low-cost python logic performs ds-identify determine if the environment is not a match for this datasource so it can get out fast
 2. ds.crawl_metadata(): perform a read-only walk (not affecting the datasource caches) of all related instance data sources returning a dict
 3. ds.get_data() renamed to ds.process_data(crawled_data):
    a. will take crawled_metadata dictionary which will be processed and persisted as instance attributes metadata, vendordata, userdata.
    b. write instance-data.json
    c. cache crawled_data as ds._crawled_data for future comparison by metadata_changed method
 4. ds.metadata_changed(crawled_data): compares crawled_data to internal cached crawled_data returning a list of keys which changed content

    update_metadata will then look like this:

    process_data
        if supported_events:
            if hasattr(self, 'detect'):
                 if not ds.detect():
                     return False
            crawled_data = self.crawl_metadata()
            if self.metadata_changed(crawled_data):
                result = self.process_data(crawled_data)

8b5ce65... by Chad Smith

renamed maintenance_events -> update_events.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

i'm a +1 with this generally.

c-i says it needs fixing. but I think i'm ok with the basic framework at this point.

review: Approve
61355a6... by Chad Smith

flakes: rename local events variable to avoid name collision

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:61355a62829261b251f9741bced741483a4ce53c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/136/
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/136/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:61355a62829261b251f9741bced741483a4ce53c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/137/
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/137/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

This merge has landed in commit be9ecc12 to cloud-init branch master.

To view that commit see the following URL:
https://git.launchpad.net/cloud-init/commit/?id=be9ecc12

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/event.py b/cloudinit/event.py
2new file mode 100644
3index 0000000..f7b311f
4--- /dev/null
5+++ b/cloudinit/event.py
6@@ -0,0 +1,17 @@
7+# This file is part of cloud-init. See LICENSE file for license information.
8+
9+"""Classes and functions related to event handling."""
10+
11+
12+# Event types which can generate maintenance requests for cloud-init.
13+class EventType(object):
14+ BOOT = "System boot"
15+ BOOT_NEW_INSTANCE = "New instance first boot"
16+
17+ # TODO: Cloud-init will grow support for the follow event types:
18+ # UDEV
19+ # METADATA_CHANGE
20+ # USER_REQUEST
21+
22+
23+# vi: ts=4 expandtab
24diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
25index 90d7457..f424316 100644
26--- a/cloudinit/sources/__init__.py
27+++ b/cloudinit/sources/__init__.py
28@@ -19,6 +19,7 @@ from cloudinit.atomic_helper import write_json
29 from cloudinit import importer
30 from cloudinit import log as logging
31 from cloudinit import net
32+from cloudinit.event import EventType
33 from cloudinit import type_utils
34 from cloudinit import user_data as ud
35 from cloudinit import util
36@@ -102,6 +103,25 @@ class DataSource(object):
37 url_timeout = 10 # timeout for each metadata url read attempt
38 url_retries = 5 # number of times to retry url upon 404
39
40+ # The datasource defines a list of supported EventTypes during which
41+ # the datasource can react to changes in metadata and regenerate
42+ # network configuration on metadata changes.
43+ # A datasource which supports writing network config on each system boot
44+ # would set update_events = {'network': [EventType.BOOT]}
45+
46+ # Default: generate network config on new instance id (first boot).
47+ update_events = {'network': [EventType.BOOT_NEW_INSTANCE]}
48+
49+ # N-tuple listing default values for any metadata-related class
50+ # attributes cached on an instance by a process_data runs. These attribute
51+ # values are reset via clear_cached_attrs during any update_metadata call.
52+ cached_attr_defaults = (
53+ ('ec2_metadata', UNSET), ('network_json', UNSET),
54+ ('metadata', {}), ('userdata', None), ('userdata_raw', None),
55+ ('vendordata', None), ('vendordata_raw', None))
56+
57+ _dirty_cache = False
58+
59 def __init__(self, sys_cfg, distro, paths, ud_proc=None):
60 self.sys_cfg = sys_cfg
61 self.distro = distro
62@@ -134,11 +154,31 @@ class DataSource(object):
63 'region': self.region,
64 'availability-zone': self.availability_zone}}
65
66+ def clear_cached_attrs(self, attr_defaults=()):
67+ """Reset any cached metadata attributes to datasource defaults.
68+
69+ @param attr_defaults: Optional tuple of (attr, value) pairs to
70+ set instead of cached_attr_defaults.
71+ """
72+ if not self._dirty_cache:
73+ return
74+ if attr_defaults:
75+ attr_values = attr_defaults
76+ else:
77+ attr_values = self.cached_attr_defaults
78+
79+ for attribute, value in attr_values:
80+ if hasattr(self, attribute):
81+ setattr(self, attribute, value)
82+ if not attr_defaults:
83+ self._dirty_cache = False
84+
85 def get_data(self):
86 """Datasources implement _get_data to setup metadata and userdata_raw.
87
88 Minimally, the datasource should return a boolean True on success.
89 """
90+ self._dirty_cache = True
91 return_value = self._get_data()
92 json_file = os.path.join(self.paths.run_dir, INSTANCE_JSON_FILE)
93 if not return_value:
94@@ -174,6 +214,7 @@ class DataSource(object):
95 return return_value
96
97 def _get_data(self):
98+ """Walk metadata sources, process crawled data and save attributes."""
99 raise NotImplementedError(
100 'Subclasses of DataSource must implement _get_data which'
101 ' sets self.metadata, vendordata_raw and userdata_raw.')
102@@ -416,6 +457,41 @@ class DataSource(object):
103 def get_package_mirror_info(self):
104 return self.distro.get_package_mirror_info(data_source=self)
105
106+ def update_metadata(self, source_event_types):
107+ """Refresh cached metadata if the datasource supports this event.
108+
109+ The datasource has a list of update_events which
110+ trigger refreshing all cached metadata as well as refreshing the
111+ network configuration.
112+
113+ @param source_event_types: List of EventTypes which may trigger a
114+ metadata update.
115+
116+ @return True if the datasource did successfully update cached metadata
117+ due to source_event_type.
118+ """
119+ supported_events = {}
120+ for event in source_event_types:
121+ for update_scope, update_events in self.update_events.items():
122+ if event in update_events:
123+ if not supported_events.get(update_scope):
124+ supported_events[update_scope] = []
125+ supported_events[update_scope].append(event)
126+ for scope, matched_events in supported_events.items():
127+ LOG.debug(
128+ "Update datasource metadata and %s config due to events: %s",
129+ scope, ', '.join(matched_events))
130+ # Each datasource has a cached config property which needs clearing
131+ # Once cleared that config property will be regenerated from
132+ # current metadata.
133+ self.clear_cached_attrs((('_%s_config' % scope, UNSET),))
134+ if supported_events:
135+ self.clear_cached_attrs()
136+ result = self.get_data()
137+ if result:
138+ return True
139+ return False
140+
141 def check_instance_id(self, sys_cfg):
142 # quickly (local check only) if self.instance_id is still
143 return False
144@@ -520,7 +596,7 @@ def find_source(sys_cfg, distro, paths, ds_deps, cfg_list, pkg_list, reporter):
145 with myrep:
146 LOG.debug("Seeing if we can get any data from %s", cls)
147 s = cls(sys_cfg, distro, paths)
148- if s.get_data():
149+ if s.update_metadata([EventType.BOOT_NEW_INSTANCE]):
150 myrep.message = "found %s data from %s" % (mode, name)
151 return (s, type_utils.obj_name(cls))
152 except Exception:
153diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
154index d5bc98a..dcd221b 100644
155--- a/cloudinit/sources/tests/test_init.py
156+++ b/cloudinit/sources/tests/test_init.py
157@@ -5,10 +5,11 @@ import os
158 import six
159 import stat
160
161+from cloudinit.event import EventType
162 from cloudinit.helpers import Paths
163 from cloudinit import importer
164 from cloudinit.sources import (
165- INSTANCE_JSON_FILE, DataSource)
166+ INSTANCE_JSON_FILE, DataSource, UNSET)
167 from cloudinit.tests.helpers import CiTestCase, skipIf, mock
168 from cloudinit.user_data import UserDataProcessor
169 from cloudinit import util
170@@ -381,3 +382,83 @@ class TestDataSource(CiTestCase):
171 get_args(grandchild.get_hostname), # pylint: disable=W1505
172 '%s does not implement DataSource.get_hostname params'
173 % grandchild)
174+
175+ def test_clear_cached_attrs_resets_cached_attr_class_attributes(self):
176+ """Class attributes listed in cached_attr_defaults are reset."""
177+ count = 0
178+ # Setup values for all cached class attributes
179+ for attr, value in self.datasource.cached_attr_defaults:
180+ setattr(self.datasource, attr, count)
181+ count += 1
182+ self.datasource._dirty_cache = True
183+ self.datasource.clear_cached_attrs()
184+ for attr, value in self.datasource.cached_attr_defaults:
185+ self.assertEqual(value, getattr(self.datasource, attr))
186+
187+ def test_clear_cached_attrs_noops_on_clean_cache(self):
188+ """Class attributes listed in cached_attr_defaults are reset."""
189+ count = 0
190+ # Setup values for all cached class attributes
191+ for attr, _ in self.datasource.cached_attr_defaults:
192+ setattr(self.datasource, attr, count)
193+ count += 1
194+ self.datasource._dirty_cache = False # Fake clean cache
195+ self.datasource.clear_cached_attrs()
196+ count = 0
197+ for attr, _ in self.datasource.cached_attr_defaults:
198+ self.assertEqual(count, getattr(self.datasource, attr))
199+ count += 1
200+
201+ def test_clear_cached_attrs_skips_non_attr_class_attributes(self):
202+ """Skip any cached_attr_defaults which aren't class attributes."""
203+ self.datasource._dirty_cache = True
204+ self.datasource.clear_cached_attrs()
205+ for attr in ('ec2_metadata', 'network_json'):
206+ self.assertFalse(hasattr(self.datasource, attr))
207+
208+ def test_clear_cached_attrs_of_custom_attrs(self):
209+ """Custom attr_values can be passed to clear_cached_attrs."""
210+ self.datasource._dirty_cache = True
211+ cached_attr_name = self.datasource.cached_attr_defaults[0][0]
212+ setattr(self.datasource, cached_attr_name, 'himom')
213+ self.datasource.myattr = 'orig'
214+ self.datasource.clear_cached_attrs(
215+ attr_defaults=(('myattr', 'updated'),))
216+ self.assertEqual('himom', getattr(self.datasource, cached_attr_name))
217+ self.assertEqual('updated', self.datasource.myattr)
218+
219+ def test_update_metadata_only_acts_on_supported_update_events(self):
220+ """update_metadata won't get_data on unsupported update events."""
221+ self.assertEqual(
222+ {'network': [EventType.BOOT_NEW_INSTANCE]},
223+ self.datasource.update_events)
224+
225+ def fake_get_data():
226+ raise Exception('get_data should not be called')
227+
228+ self.datasource.get_data = fake_get_data
229+ self.assertFalse(
230+ self.datasource.update_metadata(
231+ source_event_types=[EventType.BOOT]))
232+
233+ def test_update_metadata_returns_true_on_supported_update_event(self):
234+ """update_metadata returns get_data response on supported events."""
235+
236+ def fake_get_data():
237+ return True
238+
239+ self.datasource.get_data = fake_get_data
240+ self.datasource._network_config = 'something'
241+ self.datasource._dirty_cache = True
242+ self.assertTrue(
243+ self.datasource.update_metadata(
244+ source_event_types=[
245+ EventType.BOOT, EventType.BOOT_NEW_INSTANCE]))
246+ self.assertEqual(UNSET, self.datasource._network_config)
247+ self.assertIn(
248+ "DEBUG: Update datasource metadata and network config due to"
249+ " events: New instance first boot",
250+ self.logs.getvalue())
251+
252+
253+# vi: ts=4 expandtab
254diff --git a/cloudinit/stages.py b/cloudinit/stages.py
255index 286607b..c132b57 100644
256--- a/cloudinit/stages.py
257+++ b/cloudinit/stages.py
258@@ -22,6 +22,8 @@ from cloudinit.handlers import cloud_config as cc_part
259 from cloudinit.handlers import shell_script as ss_part
260 from cloudinit.handlers import upstart_job as up_part
261
262+from cloudinit.event import EventType
263+
264 from cloudinit import cloud
265 from cloudinit import config
266 from cloudinit import distros
267@@ -648,10 +650,14 @@ class Init(object):
268 except Exception as e:
269 LOG.warning("Failed to rename devices: %s", e)
270
271- if (self.datasource is not NULL_DATA_SOURCE and
272- not self.is_new_instance()):
273- LOG.debug("not a new instance. network config is not applied.")
274- return
275+ if self.datasource is not NULL_DATA_SOURCE:
276+ if not self.is_new_instance():
277+ if not self.datasource.update_metadata([EventType.BOOT]):
278+ LOG.debug(
279+ "No network config applied. Neither a new instance"
280+ " nor datasource network update on '%s' event",
281+ EventType.BOOT)
282+ return
283
284 LOG.info("Applying network configuration from %s bringup=%s: %s",
285 src, bring_up, netcfg)
286diff --git a/cloudinit/tests/test_stages.py b/cloudinit/tests/test_stages.py
287new file mode 100644
288index 0000000..94b6b25
289--- /dev/null
290+++ b/cloudinit/tests/test_stages.py
291@@ -0,0 +1,231 @@
292+# This file is part of cloud-init. See LICENSE file for license information.
293+
294+"""Tests related to cloudinit.stages module."""
295+
296+import os
297+
298+from cloudinit import stages
299+from cloudinit import sources
300+
301+from cloudinit.event import EventType
302+from cloudinit.util import write_file
303+
304+from cloudinit.tests.helpers import CiTestCase, mock
305+
306+TEST_INSTANCE_ID = 'i-testing'
307+
308+
309+class FakeDataSource(sources.DataSource):
310+
311+ def __init__(self, paths=None, userdata=None, vendordata=None,
312+ network_config=''):
313+ super(FakeDataSource, self).__init__({}, None, paths=paths)
314+ self.metadata = {'instance-id': TEST_INSTANCE_ID}
315+ self.userdata_raw = userdata
316+ self.vendordata_raw = vendordata
317+ self._network_config = None
318+ if network_config: # Permit for None value to setup attribute
319+ self._network_config = network_config
320+
321+ @property
322+ def network_config(self):
323+ return self._network_config
324+
325+ def _get_data(self):
326+ return True
327+
328+
329+class TestInit(CiTestCase):
330+ with_logs = True
331+
332+ def setUp(self):
333+ super(TestInit, self).setUp()
334+ self.tmpdir = self.tmp_dir()
335+ self.init = stages.Init()
336+ # Setup fake Paths for Init to reference
337+ self.init._cfg = {'system_info': {
338+ 'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir,
339+ 'run_dir': self.tmpdir}}}
340+ self.init.datasource = FakeDataSource(paths=self.init.paths)
341+
342+ def test_wb__find_networking_config_disabled(self):
343+ """find_networking_config returns no config when disabled."""
344+ disable_file = os.path.join(
345+ self.init.paths.get_cpath('data'), 'upgraded-network')
346+ write_file(disable_file, '')
347+ self.assertEqual(
348+ (None, disable_file),
349+ self.init._find_networking_config())
350+
351+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
352+ def test_wb__find_networking_config_disabled_by_kernel(self, m_cmdline):
353+ """find_networking_config returns when disabled by kernel cmdline."""
354+ m_cmdline.return_value = {'config': 'disabled'}
355+ self.assertEqual(
356+ (None, 'cmdline'),
357+ self.init._find_networking_config())
358+ self.assertEqual('DEBUG: network config disabled by cmdline\n',
359+ self.logs.getvalue())
360+
361+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
362+ def test_wb__find_networking_config_disabled_by_datasrc(self, m_cmdline):
363+ """find_networking_config returns when disabled by datasource cfg."""
364+ m_cmdline.return_value = {} # Kernel doesn't disable networking
365+ self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
366+ 'network': {}} # system config doesn't disable
367+
368+ self.init.datasource = FakeDataSource(
369+ network_config={'config': 'disabled'})
370+ self.assertEqual(
371+ (None, 'ds'),
372+ self.init._find_networking_config())
373+ self.assertEqual('DEBUG: network config disabled by ds\n',
374+ self.logs.getvalue())
375+
376+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
377+ def test_wb__find_networking_config_disabled_by_sysconfig(self, m_cmdline):
378+ """find_networking_config returns when disabled by system config."""
379+ m_cmdline.return_value = {} # Kernel doesn't disable networking
380+ self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
381+ 'network': {'config': 'disabled'}}
382+ self.assertEqual(
383+ (None, 'system_cfg'),
384+ self.init._find_networking_config())
385+ self.assertEqual('DEBUG: network config disabled by system_cfg\n',
386+ self.logs.getvalue())
387+
388+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
389+ def test_wb__find_networking_config_returns_kernel(self, m_cmdline):
390+ """find_networking_config returns kernel cmdline config if present."""
391+ expected_cfg = {'config': ['fakekernel']}
392+ m_cmdline.return_value = expected_cfg
393+ self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
394+ 'network': {'config': ['fakesys_config']}}
395+ self.init.datasource = FakeDataSource(
396+ network_config={'config': ['fakedatasource']})
397+ self.assertEqual(
398+ (expected_cfg, 'cmdline'),
399+ self.init._find_networking_config())
400+
401+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
402+ def test_wb__find_networking_config_returns_system_cfg(self, m_cmdline):
403+ """find_networking_config returns system config when present."""
404+ m_cmdline.return_value = {} # No kernel network config
405+ expected_cfg = {'config': ['fakesys_config']}
406+ self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
407+ 'network': expected_cfg}
408+ self.init.datasource = FakeDataSource(
409+ network_config={'config': ['fakedatasource']})
410+ self.assertEqual(
411+ (expected_cfg, 'system_cfg'),
412+ self.init._find_networking_config())
413+
414+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
415+ def test_wb__find_networking_config_returns_datasrc_cfg(self, m_cmdline):
416+ """find_networking_config returns datasource net config if present."""
417+ m_cmdline.return_value = {} # No kernel network config
418+ # No system config for network in setUp
419+ expected_cfg = {'config': ['fakedatasource']}
420+ self.init.datasource = FakeDataSource(network_config=expected_cfg)
421+ self.assertEqual(
422+ (expected_cfg, 'ds'),
423+ self.init._find_networking_config())
424+
425+ @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
426+ def test_wb__find_networking_config_returns_fallback(self, m_cmdline):
427+ """find_networking_config returns fallback config if not defined."""
428+ m_cmdline.return_value = {} # Kernel doesn't disable networking
429+ # Neither datasource nor system_info disable or provide network
430+
431+ fake_cfg = {'config': [{'type': 'physical', 'name': 'eth9'}],
432+ 'version': 1}
433+
434+ def fake_generate_fallback():
435+ return fake_cfg
436+
437+ # Monkey patch distro which gets cached on self.init
438+ distro = self.init.distro
439+ distro.generate_fallback_config = fake_generate_fallback
440+ self.assertEqual(
441+ (fake_cfg, 'fallback'),
442+ self.init._find_networking_config())
443+ self.assertNotIn('network config disabled', self.logs.getvalue())
444+
445+ def test_apply_network_config_disabled(self):
446+ """Log when network is disabled by upgraded-network."""
447+ disable_file = os.path.join(
448+ self.init.paths.get_cpath('data'), 'upgraded-network')
449+
450+ def fake_network_config():
451+ return (None, disable_file)
452+
453+ self.init._find_networking_config = fake_network_config
454+
455+ self.init.apply_network_config(True)
456+ self.assertIn(
457+ 'INFO: network config is disabled by %s' % disable_file,
458+ self.logs.getvalue())
459+
460+ @mock.patch('cloudinit.distros.ubuntu.Distro')
461+ def test_apply_network_on_new_instance(self, m_ubuntu):
462+ """Call distro apply_network_config methods on is_new_instance."""
463+ net_cfg = {
464+ 'version': 1, 'config': [
465+ {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
466+ 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
467+
468+ def fake_network_config():
469+ return net_cfg, 'fallback'
470+
471+ self.init._find_networking_config = fake_network_config
472+ self.init.apply_network_config(True)
473+ self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
474+ self.init.distro.apply_network_config.assert_called_with(
475+ net_cfg, bring_up=True)
476+
477+ @mock.patch('cloudinit.distros.ubuntu.Distro')
478+ def test_apply_network_on_same_instance_id(self, m_ubuntu):
479+ """Only call distro.apply_network_config_names on same instance id."""
480+ old_instance_id = os.path.join(
481+ self.init.paths.get_cpath('data'), 'instance-id')
482+ write_file(old_instance_id, TEST_INSTANCE_ID)
483+ net_cfg = {
484+ 'version': 1, 'config': [
485+ {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
486+ 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
487+
488+ def fake_network_config():
489+ return net_cfg, 'fallback'
490+
491+ self.init._find_networking_config = fake_network_config
492+ self.init.apply_network_config(True)
493+ self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
494+ self.init.distro.apply_network_config.assert_not_called()
495+ self.assertIn(
496+ 'No network config applied. Neither a new instance'
497+ " nor datasource network update on '%s' event" % EventType.BOOT,
498+ self.logs.getvalue())
499+
500+ @mock.patch('cloudinit.distros.ubuntu.Distro')
501+ def test_apply_network_on_datasource_allowed_event(self, m_ubuntu):
502+ """Apply network if datasource.update_metadata permits BOOT event."""
503+ old_instance_id = os.path.join(
504+ self.init.paths.get_cpath('data'), 'instance-id')
505+ write_file(old_instance_id, TEST_INSTANCE_ID)
506+ net_cfg = {
507+ 'version': 1, 'config': [
508+ {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
509+ 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
510+
511+ def fake_network_config():
512+ return net_cfg, 'fallback'
513+
514+ self.init._find_networking_config = fake_network_config
515+ self.init.datasource = FakeDataSource(paths=self.init.paths)
516+ self.init.datasource.update_events = {'network': [EventType.BOOT]}
517+ self.init.apply_network_config(True)
518+ self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
519+ self.init.distro.apply_network_config.assert_called_with(
520+ net_cfg, bring_up=True)
521+
522+# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches