summaryrefslogtreecommitdiff
path: root/cloudinit/cmd
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2018-03-14 23:38:07 -0600
committerChad Smith <chad.smith@canonical.com>2018-03-14 23:38:07 -0600
commit133ad2cb327ad17b7b81319fac8f9f14577c04df (patch)
treeca14c7c958537e00f1c7ce93405eb77cee85b81e /cloudinit/cmd
parent76460b63f9c310c7de4e5f0c11d1525bedd277e1 (diff)
downloadvyos-cloud-init-133ad2cb327ad17b7b81319fac8f9f14577c04df.tar.gz
vyos-cloud-init-133ad2cb327ad17b7b81319fac8f9f14577c04df.zip
set_hostname: When present in metadata, set it before network bringup.
When instance meta-data provides hostname information, run cc_set_hostname in the init-local or init-net stage before network comes up. Prevent an initial DHCP request which leaks the stock cloud-image default hostname before the meta-data provided hostname was processed. A leaked cloud-image hostname adversely affects Dynamic DNS which would reallocate 'ubuntu' hostname in DNS to every instance brought up by cloud-init. These instances would only update DNS to the cloud-init configured hostname upon DHCP lease renewal. This branch extends the get_hostname methods in datasource, cloud and util to limit results to metadata_only to avoid extra cost of querying the distro for hostname information if metadata does not provide that information. LP: #1746455
Diffstat (limited to 'cloudinit/cmd')
-rw-r--r--cloudinit/cmd/main.py25
-rw-r--r--cloudinit/cmd/tests/test_main.py161
2 files changed, 186 insertions, 0 deletions
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index fcddd75c..3f2dbb93 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -40,6 +40,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
from cloudinit import atomic_helper
+from cloudinit.config import cc_set_hostname
from cloudinit.dhclient_hook import LogDhclient
@@ -352,6 +353,11 @@ def main_init(name, args):
LOG.debug("[%s] %s will now be targeting instance id: %s. new=%s",
mode, name, iid, init.is_new_instance())
+ if mode == sources.DSMODE_LOCAL:
+ # Before network comes up, set any configured hostname to allow
+ # dhcp clients to advertize this hostname to any DDNS services
+ # LP: #1746455.
+ _maybe_set_hostname(init, stage='local', retry_stage='network')
init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL))
if mode == sources.DSMODE_LOCAL:
@@ -368,6 +374,7 @@ def main_init(name, args):
init.setup_datasource()
# update fully realizes user-data (pulling in #include if necessary)
init.update()
+ _maybe_set_hostname(init, stage='init-net', retry_stage='modules:config')
# Stage 7
try:
# Attempt to consume the data per instance.
@@ -681,6 +688,24 @@ def status_wrapper(name, args, data_d=None, link_d=None):
return len(v1[mode]['errors'])
+def _maybe_set_hostname(init, stage, retry_stage):
+ """Call set-hostname if metadata, vendordata or userdata provides it.
+
+ @param stage: String representing current stage in which we are running.
+ @param retry_stage: String represented logs upon error setting hostname.
+ """
+ cloud = init.cloudify()
+ (hostname, _fqdn) = util.get_hostname_fqdn(
+ init.cfg, cloud, metadata_only=True)
+ if hostname: # meta-data or user-data hostname content
+ try:
+ cc_set_hostname.handle('set-hostname', init.cfg, cloud, LOG, None)
+ except cc_set_hostname.SetHostnameError as e:
+ LOG.debug(
+ 'Failed setting hostname in %s stage. Will'
+ ' retry in %s stage. Error: %s.', stage, retry_stage, str(e))
+
+
def main_features(name, args):
sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n')
diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py
new file mode 100644
index 00000000..dbe421c0
--- /dev/null
+++ b/cloudinit/cmd/tests/test_main.py
@@ -0,0 +1,161 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from collections import namedtuple
+import copy
+import os
+from six import StringIO
+
+from cloudinit.cmd import main
+from cloudinit.util import (
+ ensure_dir, load_file, write_file, yaml_dumps)
+from cloudinit.tests.helpers import (
+ FilesystemMockingTestCase, wrap_and_call)
+
+mypaths = namedtuple('MyPaths', 'run_dir')
+myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand')
+
+
+class TestMain(FilesystemMockingTestCase):
+
+ with_logs = True
+
+ def setUp(self):
+ super(TestMain, self).setUp()
+ self.new_root = self.tmp_dir()
+ self.cloud_dir = self.tmp_path('var/lib/cloud/', dir=self.new_root)
+ os.makedirs(self.cloud_dir)
+ self.replicateTestRoot('simple_ubuntu', self.new_root)
+ self.cfg = {
+ 'datasource_list': ['None'],
+ 'runcmd': ['ls /etc'], # test ALL_DISTROS
+ 'system_info': {'paths': {'cloud_dir': self.cloud_dir,
+ 'run_dir': self.new_root}},
+ 'write_files': [
+ {
+ 'path': '/etc/blah.ini',
+ 'content': 'blah',
+ 'permissions': 0o755,
+ },
+ ],
+ 'cloud_init_modules': ['write-files', 'runcmd'],
+ }
+ cloud_cfg = yaml_dumps(self.cfg)
+ ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
+ self.cloud_cfg_file = os.path.join(
+ self.new_root, 'etc', 'cloud', 'cloud.cfg')
+ write_file(self.cloud_cfg_file, cloud_cfg)
+ self.patchOS(self.new_root)
+ self.patchUtils(self.new_root)
+ self.stderr = StringIO()
+ self.patchStdoutAndStderr(stderr=self.stderr)
+
+ def test_main_init_run_net_stops_on_file_no_net(self):
+ """When no-net file is present, main_init does not process modules."""
+ stop_file = os.path.join(self.cloud_dir, 'data', 'no-net') # stop file
+ write_file(stop_file, '')
+ cmdargs = myargs(
+ debug=False, files=None, force=False, local=False, reporter=None,
+ subcommand='init')
+ (item1, item2) = wrap_and_call(
+ 'cloudinit.cmd.main',
+ {'util.close_stdin': True,
+ 'netinfo.debug_info': 'my net debug info',
+ 'util.fixup_output': ('outfmt', 'errfmt')},
+ main.main_init, 'init', cmdargs)
+ # We should not run write_files module
+ self.assertFalse(
+ os.path.exists(os.path.join(self.new_root, 'etc/blah.ini')),
+ 'Unexpected run of write_files module produced blah.ini')
+ self.assertEqual([], item2)
+ # Instancify is called
+ instance_id_path = 'var/lib/cloud/data/instance-id'
+ self.assertFalse(
+ os.path.exists(os.path.join(self.new_root, instance_id_path)),
+ 'Unexpected call to datasource.instancify produced instance-id')
+ expected_logs = [
+ "Exiting. stop file ['{stop_file}'] existed\n".format(
+ stop_file=stop_file),
+ 'my net debug info' # netinfo.debug_info
+ ]
+ for log in expected_logs:
+ self.assertIn(log, self.stderr.getvalue())
+
+ def test_main_init_run_net_runs_modules(self):
+ """Modules like write_files are run in 'net' mode."""
+ cmdargs = myargs(
+ debug=False, files=None, force=False, local=False, reporter=None,
+ subcommand='init')
+ (item1, item2) = wrap_and_call(
+ 'cloudinit.cmd.main',
+ {'util.close_stdin': True,
+ 'netinfo.debug_info': 'my net debug info',
+ 'util.fixup_output': ('outfmt', 'errfmt')},
+ main.main_init, 'init', cmdargs)
+ self.assertEqual([], item2)
+ # Instancify is called
+ instance_id_path = 'var/lib/cloud/data/instance-id'
+ self.assertEqual(
+ 'iid-datasource-none\n',
+ os.path.join(load_file(
+ os.path.join(self.new_root, instance_id_path))))
+ # modules are run (including write_files)
+ self.assertEqual(
+ 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
+ expected_logs = [
+ 'network config is disabled by fallback', # apply_network_config
+ 'my net debug info', # netinfo.debug_info
+ 'no previous run detected'
+ ]
+ for log in expected_logs:
+ self.assertIn(log, self.stderr.getvalue())
+
+ def test_main_init_run_net_calls_set_hostname_when_metadata_present(self):
+ """When local-hostname metadata is present, call cc_set_hostname."""
+ self.cfg['datasource'] = {
+ 'None': {'metadata': {'local-hostname': 'md-hostname'}}}
+ cloud_cfg = yaml_dumps(self.cfg)
+ write_file(self.cloud_cfg_file, cloud_cfg)
+ cmdargs = myargs(
+ debug=False, files=None, force=False, local=False, reporter=None,
+ subcommand='init')
+
+ def set_hostname(name, cfg, cloud, log, args):
+ self.assertEqual('set-hostname', name)
+ updated_cfg = copy.deepcopy(self.cfg)
+ updated_cfg.update(
+ {'def_log_file': '/var/log/cloud-init.log',
+ 'log_cfgs': [],
+ 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel'],
+ 'vendor_data': {'enabled': True, 'prefix': []}})
+ updated_cfg.pop('system_info')
+
+ self.assertEqual(updated_cfg, cfg)
+ self.assertEqual(main.LOG, log)
+ self.assertIsNone(args)
+
+ (item1, item2) = wrap_and_call(
+ 'cloudinit.cmd.main',
+ {'util.close_stdin': True,
+ 'netinfo.debug_info': 'my net debug info',
+ 'cc_set_hostname.handle': {'side_effect': set_hostname},
+ 'util.fixup_output': ('outfmt', 'errfmt')},
+ main.main_init, 'init', cmdargs)
+ self.assertEqual([], item2)
+ # Instancify is called
+ instance_id_path = 'var/lib/cloud/data/instance-id'
+ self.assertEqual(
+ 'iid-datasource-none\n',
+ os.path.join(load_file(
+ os.path.join(self.new_root, instance_id_path))))
+ # modules are run (including write_files)
+ self.assertEqual(
+ 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
+ expected_logs = [
+ 'network config is disabled by fallback', # apply_network_config
+ 'my net debug info', # netinfo.debug_info
+ 'no previous run detected'
+ ]
+ for log in expected_logs:
+ self.assertIn(log, self.stderr.getvalue())
+
+# vi: ts=4 expandtab