Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

Proposed by Chad Smith
Status: Merged
Merged at revision: a6262577f56d32fb6005a55f9022309c5dc7dce5
Proposed branch: ~chad.smith/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 1999 lines (+765/-204)
46 files modified
.pylintrc (+11/-1)
cloudinit/cloud.py (+3/-2)
cloudinit/cmd/main.py (+29/-6)
cloudinit/cmd/tests/test_main.py (+161/-0)
cloudinit/config/cc_keys_to_console.py (+1/-3)
cloudinit/config/cc_runcmd.py (+4/-2)
cloudinit/config/cc_salt_minion.py (+59/-23)
cloudinit/config/cc_set_hostname.py (+35/-6)
cloudinit/config/cc_ssh_authkey_fingerprints.py (+4/-5)
cloudinit/distros/arch.py (+1/-4)
cloudinit/distros/freebsd.py (+6/-0)
cloudinit/distros/opensuse.py (+2/-3)
cloudinit/sources/DataSourceAzure.py (+2/-0)
cloudinit/sources/DataSourceOpenNebula.py (+1/-4)
cloudinit/sources/__init__.py (+17/-4)
cloudinit/sources/tests/test_init.py (+69/-1)
cloudinit/stages.py (+1/-2)
cloudinit/tests/helpers.py (+13/-0)
cloudinit/tests/test_util.py (+97/-0)
cloudinit/url_helper.py (+2/-2)
cloudinit/util.py (+32/-14)
config/cloud.cfg.tmpl (+1/-1)
debian/changelog (+22/-0)
doc/rtd/topics/capabilities.rst (+8/-6)
doc/rtd/topics/debugging.rst (+31/-26)
doc/rtd/topics/network-config.rst (+2/-2)
doc/rtd/topics/tests.rst (+10/-10)
tests/cloud_tests/bddeb.py (+1/-1)
tests/cloud_tests/platforms/ec2/__init__.py (+0/-0)
tests/cloud_tests/platforms/lxd/__init__.py (+0/-0)
tests/cloud_tests/platforms/lxd/platform.py (+0/-4)
tests/cloud_tests/platforms/nocloudkvm/__init__.py (+0/-0)
tests/cloud_tests/platforms/nocloudkvm/instance.py (+1/-1)
tests/cloud_tests/platforms/nocloudkvm/platform.py (+0/-4)
tests/cloud_tests/platforms/platforms.py (+12/-2)
tests/cloud_tests/testcases/modules/salt_minion.py (+5/-0)
tests/cloud_tests/testcases/modules/salt_minion.yaml (+4/-1)
tests/cloud_tests/util.py (+5/-1)
tests/unittests/test_datasource/test_azure.py (+15/-0)
tests/unittests/test_handler/test_handler_bootcmd.py (+7/-12)
tests/unittests/test_handler/test_handler_ntp.py (+6/-12)
tests/unittests/test_handler/test_handler_resizefs.py (+3/-11)
tests/unittests/test_handler/test_handler_runcmd.py (+4/-10)
tests/unittests/test_handler/test_handler_set_hostname.py (+53/-4)
tests/unittests/test_handler/test_schema.py (+7/-14)
tests/unittests/test_util.py (+18/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Pending
Review via email: mp+341482@code.launchpad.net

Description of the change

Sync tip of master for publish in Bionic.

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

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.pylintrc b/.pylintrc
index 05a086d..0bdfa59 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -46,7 +46,17 @@ reports=no
46# (useful for modules/projects where namespaces are manipulated during runtime46# (useful for modules/projects where namespaces are manipulated during runtime
47# and thus existing member attributes cannot be deduced by static analysis. It47# and thus existing member attributes cannot be deduced by static analysis. It
48# supports qualified module names, as well as Unix pattern matching.48# supports qualified module names, as well as Unix pattern matching.
49ignored-modules=six.moves,pkg_resources,httplib,http.client,paramiko,simplestreams49ignored-modules=
50 http.client,
51 httplib,
52 pkg_resources,
53 six.moves,
54 # cloud_tests requirements.
55 boto3,
56 botocore,
57 paramiko,
58 pylxd,
59 simplestreams
5060
51# List of class names for which member attributes should not be checked (useful61# List of class names for which member attributes should not be checked (useful
52# for classes with dynamically set attributes). This supports the use of62# for classes with dynamically set attributes). This supports the use of
diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py
index ba61678..6d12c43 100644
--- a/cloudinit/cloud.py
+++ b/cloudinit/cloud.py
@@ -78,8 +78,9 @@ class Cloud(object):
78 def get_locale(self):78 def get_locale(self):
79 return self.datasource.get_locale()79 return self.datasource.get_locale()
8080
81 def get_hostname(self, fqdn=False):81 def get_hostname(self, fqdn=False, metadata_only=False):
82 return self.datasource.get_hostname(fqdn=fqdn)82 return self.datasource.get_hostname(
83 fqdn=fqdn, metadata_only=metadata_only)
8384
84 def device_name_to_device(self, name):85 def device_name_to_device(self, name):
85 return self.datasource.device_name_to_device(name)86 return self.datasource.device_name_to_device(name)
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index d2f1b77..3f2dbb9 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,
4040
41from cloudinit import atomic_helper41from cloudinit import atomic_helper
4242
43from cloudinit.config import cc_set_hostname
43from cloudinit.dhclient_hook import LogDhclient44from cloudinit.dhclient_hook import LogDhclient
4445
4546
@@ -215,12 +216,10 @@ def main_init(name, args):
215 if args.local:216 if args.local:
216 deps = [sources.DEP_FILESYSTEM]217 deps = [sources.DEP_FILESYSTEM]
217218
218 early_logs = []219 early_logs = [attempt_cmdline_url(
219 early_logs.append(220 path=os.path.join("%s.d" % CLOUD_CONFIG,
220 attempt_cmdline_url(221 "91_kernel_cmdline_url.cfg"),
221 path=os.path.join("%s.d" % CLOUD_CONFIG,222 network=not args.local)]
222 "91_kernel_cmdline_url.cfg"),
223 network=not args.local))
224223
225 # Cloud-init 'init' stage is broken up into the following sub-stages224 # Cloud-init 'init' stage is broken up into the following sub-stages
226 # 1. Ensure that the init object fetches its config without errors225 # 1. Ensure that the init object fetches its config without errors
@@ -354,6 +353,11 @@ def main_init(name, args):
354 LOG.debug("[%s] %s will now be targeting instance id: %s. new=%s",353 LOG.debug("[%s] %s will now be targeting instance id: %s. new=%s",
355 mode, name, iid, init.is_new_instance())354 mode, name, iid, init.is_new_instance())
356355
356 if mode == sources.DSMODE_LOCAL:
357 # Before network comes up, set any configured hostname to allow
358 # dhcp clients to advertize this hostname to any DDNS services
359 # LP: #1746455.
360 _maybe_set_hostname(init, stage='local', retry_stage='network')
357 init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL))361 init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL))
358362
359 if mode == sources.DSMODE_LOCAL:363 if mode == sources.DSMODE_LOCAL:
@@ -370,6 +374,7 @@ def main_init(name, args):
370 init.setup_datasource()374 init.setup_datasource()
371 # update fully realizes user-data (pulling in #include if necessary)375 # update fully realizes user-data (pulling in #include if necessary)
372 init.update()376 init.update()
377 _maybe_set_hostname(init, stage='init-net', retry_stage='modules:config')
373 # Stage 7378 # Stage 7
374 try:379 try:
375 # Attempt to consume the data per instance.380 # Attempt to consume the data per instance.
@@ -683,6 +688,24 @@ def status_wrapper(name, args, data_d=None, link_d=None):
683 return len(v1[mode]['errors'])688 return len(v1[mode]['errors'])
684689
685690
691def _maybe_set_hostname(init, stage, retry_stage):
692 """Call set-hostname if metadata, vendordata or userdata provides it.
693
694 @param stage: String representing current stage in which we are running.
695 @param retry_stage: String represented logs upon error setting hostname.
696 """
697 cloud = init.cloudify()
698 (hostname, _fqdn) = util.get_hostname_fqdn(
699 init.cfg, cloud, metadata_only=True)
700 if hostname: # meta-data or user-data hostname content
701 try:
702 cc_set_hostname.handle('set-hostname', init.cfg, cloud, LOG, None)
703 except cc_set_hostname.SetHostnameError as e:
704 LOG.debug(
705 'Failed setting hostname in %s stage. Will'
706 ' retry in %s stage. Error: %s.', stage, retry_stage, str(e))
707
708
686def main_features(name, args):709def main_features(name, args):
687 sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n')710 sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n')
688711
diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py
689new file mode 100644712new file mode 100644
index 0000000..dbe421c
--- /dev/null
+++ b/cloudinit/cmd/tests/test_main.py
@@ -0,0 +1,161 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3from collections import namedtuple
4import copy
5import os
6from six import StringIO
7
8from cloudinit.cmd import main
9from cloudinit.util import (
10 ensure_dir, load_file, write_file, yaml_dumps)
11from cloudinit.tests.helpers import (
12 FilesystemMockingTestCase, wrap_and_call)
13
14mypaths = namedtuple('MyPaths', 'run_dir')
15myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand')
16
17
18class TestMain(FilesystemMockingTestCase):
19
20 with_logs = True
21
22 def setUp(self):
23 super(TestMain, self).setUp()
24 self.new_root = self.tmp_dir()
25 self.cloud_dir = self.tmp_path('var/lib/cloud/', dir=self.new_root)
26 os.makedirs(self.cloud_dir)
27 self.replicateTestRoot('simple_ubuntu', self.new_root)
28 self.cfg = {
29 'datasource_list': ['None'],
30 'runcmd': ['ls /etc'], # test ALL_DISTROS
31 'system_info': {'paths': {'cloud_dir': self.cloud_dir,
32 'run_dir': self.new_root}},
33 'write_files': [
34 {
35 'path': '/etc/blah.ini',
36 'content': 'blah',
37 'permissions': 0o755,
38 },
39 ],
40 'cloud_init_modules': ['write-files', 'runcmd'],
41 }
42 cloud_cfg = yaml_dumps(self.cfg)
43 ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
44 self.cloud_cfg_file = os.path.join(
45 self.new_root, 'etc', 'cloud', 'cloud.cfg')
46 write_file(self.cloud_cfg_file, cloud_cfg)
47 self.patchOS(self.new_root)
48 self.patchUtils(self.new_root)
49 self.stderr = StringIO()
50 self.patchStdoutAndStderr(stderr=self.stderr)
51
52 def test_main_init_run_net_stops_on_file_no_net(self):
53 """When no-net file is present, main_init does not process modules."""
54 stop_file = os.path.join(self.cloud_dir, 'data', 'no-net') # stop file
55 write_file(stop_file, '')
56 cmdargs = myargs(
57 debug=False, files=None, force=False, local=False, reporter=None,
58 subcommand='init')
59 (item1, item2) = wrap_and_call(
60 'cloudinit.cmd.main',
61 {'util.close_stdin': True,
62 'netinfo.debug_info': 'my net debug info',
63 'util.fixup_output': ('outfmt', 'errfmt')},
64 main.main_init, 'init', cmdargs)
65 # We should not run write_files module
66 self.assertFalse(
67 os.path.exists(os.path.join(self.new_root, 'etc/blah.ini')),
68 'Unexpected run of write_files module produced blah.ini')
69 self.assertEqual([], item2)
70 # Instancify is called
71 instance_id_path = 'var/lib/cloud/data/instance-id'
72 self.assertFalse(
73 os.path.exists(os.path.join(self.new_root, instance_id_path)),
74 'Unexpected call to datasource.instancify produced instance-id')
75 expected_logs = [
76 "Exiting. stop file ['{stop_file}'] existed\n".format(
77 stop_file=stop_file),
78 'my net debug info' # netinfo.debug_info
79 ]
80 for log in expected_logs:
81 self.assertIn(log, self.stderr.getvalue())
82
83 def test_main_init_run_net_runs_modules(self):
84 """Modules like write_files are run in 'net' mode."""
85 cmdargs = myargs(
86 debug=False, files=None, force=False, local=False, reporter=None,
87 subcommand='init')
88 (item1, item2) = wrap_and_call(
89 'cloudinit.cmd.main',
90 {'util.close_stdin': True,
91 'netinfo.debug_info': 'my net debug info',
92 'util.fixup_output': ('outfmt', 'errfmt')},
93 main.main_init, 'init', cmdargs)
94 self.assertEqual([], item2)
95 # Instancify is called
96 instance_id_path = 'var/lib/cloud/data/instance-id'
97 self.assertEqual(
98 'iid-datasource-none\n',
99 os.path.join(load_file(
100 os.path.join(self.new_root, instance_id_path))))
101 # modules are run (including write_files)
102 self.assertEqual(
103 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
104 expected_logs = [
105 'network config is disabled by fallback', # apply_network_config
106 'my net debug info', # netinfo.debug_info
107 'no previous run detected'
108 ]
109 for log in expected_logs:
110 self.assertIn(log, self.stderr.getvalue())
111
112 def test_main_init_run_net_calls_set_hostname_when_metadata_present(self):
113 """When local-hostname metadata is present, call cc_set_hostname."""
114 self.cfg['datasource'] = {
115 'None': {'metadata': {'local-hostname': 'md-hostname'}}}
116 cloud_cfg = yaml_dumps(self.cfg)
117 write_file(self.cloud_cfg_file, cloud_cfg)
118 cmdargs = myargs(
119 debug=False, files=None, force=False, local=False, reporter=None,
120 subcommand='init')
121
122 def set_hostname(name, cfg, cloud, log, args):
123 self.assertEqual('set-hostname', name)
124 updated_cfg = copy.deepcopy(self.cfg)
125 updated_cfg.update(
126 {'def_log_file': '/var/log/cloud-init.log',
127 'log_cfgs': [],
128 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel'],
129 'vendor_data': {'enabled': True, 'prefix': []}})
130 updated_cfg.pop('system_info')
131
132 self.assertEqual(updated_cfg, cfg)
133 self.assertEqual(main.LOG, log)
134 self.assertIsNone(args)
135
136 (item1, item2) = wrap_and_call(
137 'cloudinit.cmd.main',
138 {'util.close_stdin': True,
139 'netinfo.debug_info': 'my net debug info',
140 'cc_set_hostname.handle': {'side_effect': set_hostname},
141 'util.fixup_output': ('outfmt', 'errfmt')},
142 main.main_init, 'init', cmdargs)
143 self.assertEqual([], item2)
144 # Instancify is called
145 instance_id_path = 'var/lib/cloud/data/instance-id'
146 self.assertEqual(
147 'iid-datasource-none\n',
148 os.path.join(load_file(
149 os.path.join(self.new_root, instance_id_path))))
150 # modules are run (including write_files)
151 self.assertEqual(
152 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
153 expected_logs = [
154 'network config is disabled by fallback', # apply_network_config
155 'my net debug info', # netinfo.debug_info
156 'no previous run detected'
157 ]
158 for log in expected_logs:
159 self.assertIn(log, self.stderr.getvalue())
160
161# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py
index efedd4a..aff4010 100644
--- a/cloudinit/config/cc_keys_to_console.py
+++ b/cloudinit/config/cc_keys_to_console.py
@@ -63,9 +63,7 @@ def handle(name, cfg, cloud, log, _args):
63 ["ssh-dss"])63 ["ssh-dss"])
6464
65 try:65 try:
66 cmd = [helper_path]66 cmd = [helper_path, ','.join(fp_blacklist), ','.join(key_blacklist)]
67 cmd.append(','.join(fp_blacklist))
68 cmd.append(','.join(key_blacklist))
69 (stdout, _stderr) = util.subp(cmd)67 (stdout, _stderr) = util.subp(cmd)
70 util.multi_log("%s\n" % (stdout.strip()),68 util.multi_log("%s\n" % (stdout.strip()),
71 stderr=False, console=True)69 stderr=False, console=True)
diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py
index 449872f..539cbd5 100644
--- a/cloudinit/config/cc_runcmd.py
+++ b/cloudinit/config/cc_runcmd.py
@@ -39,8 +39,10 @@ schema = {
39 using ``sh``.39 using ``sh``.
4040
41 .. note::41 .. note::
42 all commands must be proper yaml, so you have to quote any characters42
43 yaml would eat (':' can be problematic)"""),43 all commands must be proper yaml, so you have to quote any characters
44 yaml would eat (':' can be problematic)
45 """),
44 'distros': distros,46 'distros': distros,
45 'examples': [dedent("""\47 'examples': [dedent("""\
46 runcmd:48 runcmd:
diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py
index 5112a34..d6a21d7 100644
--- a/cloudinit/config/cc_salt_minion.py
+++ b/cloudinit/config/cc_salt_minion.py
@@ -12,7 +12,9 @@ key is present in the config parts, then salt minion will be installed and
12started. Configuration for salt minion can be specified in the ``conf`` key12started. Configuration for salt minion can be specified in the ``conf`` key
13under ``salt_minion``. Any conf values present there will be assigned in13under ``salt_minion``. Any conf values present there will be assigned in
14``/etc/salt/minion``. The public and private keys to use for salt minion can be14``/etc/salt/minion``. The public and private keys to use for salt minion can be
15specified with ``public_key`` and ``private_key`` respectively.15specified with ``public_key`` and ``private_key`` respectively. Optionally if
16you have a custom package name, service name or config directory you can
17specify them with ``pkg_name``, ``service_name`` and ``config_dir``.
1618
17**Internal name:** ``cc_salt_minion``19**Internal name:** ``cc_salt_minion``
1820
@@ -23,6 +25,9 @@ specified with ``public_key`` and ``private_key`` respectively.
23**Config keys**::25**Config keys**::
2426
25 salt_minion:27 salt_minion:
28 pkg_name: 'salt-minion'
29 service_name: 'salt-minion'
30 config_dir: '/etc/salt'
26 conf:31 conf:
27 master: salt.example.com32 master: salt.example.com
28 grains:33 grains:
@@ -42,7 +47,34 @@ import os
4247
43from cloudinit import util48from cloudinit import util
4449
45# Note: see http://saltstack.org/topics/installation/50# Note: see https://docs.saltstack.com/en/latest/topics/installation/
51# Note: see https://docs.saltstack.com/en/latest/ref/configuration/
52
53
54class SaltConstants(object):
55 """
56 defines default distribution specific salt variables
57 """
58 def __init__(self, cfg):
59
60 # constants tailored for FreeBSD
61 if util.is_FreeBSD():
62 self.pkg_name = 'py27-salt'
63 self.srv_name = 'salt_minion'
64 self.conf_dir = '/usr/local/etc/salt'
65 # constants for any other OS
66 else:
67 self.pkg_name = 'salt-minion'
68 self.srv_name = 'salt-minion'
69 self.conf_dir = '/etc/salt'
70
71 # if there are constants given in cloud config use those
72 self.pkg_name = util.get_cfg_option_str(cfg, 'pkg_name',
73 self.pkg_name)
74 self.conf_dir = util.get_cfg_option_str(cfg, 'config_dir',
75 self.conf_dir)
76 self.srv_name = util.get_cfg_option_str(cfg, 'service_name',
77 self.srv_name)
4678
4779
48def handle(name, cfg, cloud, log, _args):80def handle(name, cfg, cloud, log, _args):
@@ -52,45 +84,49 @@ def handle(name, cfg, cloud, log, _args):
52 " no 'salt_minion' key in configuration"), name)84 " no 'salt_minion' key in configuration"), name)
53 return85 return
5486
55 salt_cfg = cfg['salt_minion']87 s_cfg = cfg['salt_minion']
88 const = SaltConstants(cfg=s_cfg)
5689
57 # Start by installing the salt package ...90 # Start by installing the salt package ...
58 cloud.distro.install_packages(('salt-minion',))91 cloud.distro.install_packages(const.pkg_name)
5992
60 # Ensure we can configure files at the right dir93 # Ensure we can configure files at the right dir
61 config_dir = salt_cfg.get("config_dir", '/etc/salt')94 util.ensure_dir(const.conf_dir)
62 util.ensure_dir(config_dir)
6395
64 # ... and then update the salt configuration96 # ... and then update the salt configuration
65 if 'conf' in salt_cfg:97 if 'conf' in s_cfg:
66 # Add all sections from the conf object to /etc/salt/minion98 # Add all sections from the conf object to minion config file
67 minion_config = os.path.join(config_dir, 'minion')99 minion_config = os.path.join(const.conf_dir, 'minion')
68 minion_data = util.yaml_dumps(salt_cfg.get('conf'))100 minion_data = util.yaml_dumps(s_cfg.get('conf'))
69 util.write_file(minion_config, minion_data)101 util.write_file(minion_config, minion_data)
70102
71 if 'grains' in salt_cfg:103 if 'grains' in s_cfg:
72 # add grains to /etc/salt/grains104 # add grains to /etc/salt/grains
73 grains_config = os.path.join(config_dir, 'grains')105 grains_config = os.path.join(const.conf_dir, 'grains')
74 grains_data = util.yaml_dumps(salt_cfg.get('grains'))106 grains_data = util.yaml_dumps(s_cfg.get('grains'))
75 util.write_file(grains_config, grains_data)107 util.write_file(grains_config, grains_data)
76108
77 # ... copy the key pair if specified109 # ... copy the key pair if specified
78 if 'public_key' in salt_cfg and 'private_key' in salt_cfg:110 if 'public_key' in s_cfg and 'private_key' in s_cfg:
79 if os.path.isdir("/etc/salt/pki/minion"):111 pki_dir_default = os.path.join(const.conf_dir, "pki/minion")
80 pki_dir_default = "/etc/salt/pki/minion"112 if not os.path.isdir(pki_dir_default):
81 else:113 pki_dir_default = os.path.join(const.conf_dir, "pki")
82 pki_dir_default = "/etc/salt/pki"
83114
84 pki_dir = salt_cfg.get('pki_dir', pki_dir_default)115 pki_dir = s_cfg.get('pki_dir', pki_dir_default)
85 with util.umask(0o77):116 with util.umask(0o77):
86 util.ensure_dir(pki_dir)117 util.ensure_dir(pki_dir)
87 pub_name = os.path.join(pki_dir, 'minion.pub')118 pub_name = os.path.join(pki_dir, 'minion.pub')
88 pem_name = os.path.join(pki_dir, 'minion.pem')119 pem_name = os.path.join(pki_dir, 'minion.pem')
89 util.write_file(pub_name, salt_cfg['public_key'])120 util.write_file(pub_name, s_cfg['public_key'])
90 util.write_file(pem_name, salt_cfg['private_key'])121 util.write_file(pem_name, s_cfg['private_key'])
122
123 # we need to have the salt minion service enabled in rc in order to be
124 # able to start the service. this does only apply on FreeBSD servers.
125 if cloud.distro.osfamily == 'freebsd':
126 cloud.distro.updatercconf('salt_minion_enable', 'YES')
91127
92 # restart salt-minion. 'service' will start even if not started. if it128 # restart salt-minion. 'service' will start even if not started. if it
93 # was started, it needs to be restarted for config change.129 # was started, it needs to be restarted for config change.
94 util.subp(['service', 'salt-minion', 'restart'], capture=False)130 util.subp(['service', const.srv_name, 'restart'], capture=False)
95131
96# vi: ts=4 expandtab132# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py
index aa3dfe5..3d2b2da 100644
--- a/cloudinit/config/cc_set_hostname.py
+++ b/cloudinit/config/cc_set_hostname.py
@@ -32,22 +32,51 @@ will be used.
32 hostname: <fqdn/hostname>32 hostname: <fqdn/hostname>
33"""33"""
3434
35import os
36
37
38from cloudinit.atomic_helper import write_json
35from cloudinit import util39from cloudinit import util
3640
3741
42class SetHostnameError(Exception):
43 """Raised when the distro runs into an exception when setting hostname.
44
45 This may happen if we attempt to set the hostname early in cloud-init's
46 init-local timeframe as certain services may not be running yet.
47 """
48 pass
49
50
38def handle(name, cfg, cloud, log, _args):51def handle(name, cfg, cloud, log, _args):
39 if util.get_cfg_option_bool(cfg, "preserve_hostname", False):52 if util.get_cfg_option_bool(cfg, "preserve_hostname", False):
40 log.debug(("Configuration option 'preserve_hostname' is set,"53 log.debug(("Configuration option 'preserve_hostname' is set,"
41 " not setting the hostname in module %s"), name)54 " not setting the hostname in module %s"), name)
42 return55 return
43
44 (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)56 (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
57 # Check for previous successful invocation of set-hostname
58
59 # set-hostname artifact file accounts for both hostname and fqdn
60 # deltas. As such, it's format is different than cc_update_hostname's
61 # previous-hostname file which only contains the base hostname.
62 # TODO consolidate previous-hostname and set-hostname artifact files and
63 # distro._read_hostname implementation so we only validate one artifact.
64 prev_fn = os.path.join(cloud.get_cpath('data'), "set-hostname")
65 prev_hostname = {}
66 if os.path.exists(prev_fn):
67 prev_hostname = util.load_json(util.load_file(prev_fn))
68 hostname_changed = (hostname != prev_hostname.get('hostname') or
69 fqdn != prev_hostname.get('fqdn'))
70 if not hostname_changed:
71 log.debug('No hostname changes. Skipping set-hostname')
72 return
73 log.debug("Setting the hostname to %s (%s)", fqdn, hostname)
45 try:74 try:
46 log.debug("Setting the hostname to %s (%s)", fqdn, hostname)
47 cloud.distro.set_hostname(hostname, fqdn)75 cloud.distro.set_hostname(hostname, fqdn)
48 except Exception:76 except Exception as e:
49 util.logexc(log, "Failed to set the hostname to %s (%s)", fqdn,77 msg = "Failed to set the hostname to %s (%s)" % (fqdn, hostname)
50 hostname)78 util.logexc(log, msg)
51 raise79 raise SetHostnameError("%s: %s" % (msg, e))
80 write_json(prev_fn, {'hostname': hostname, 'fqdn': fqdn})
5281
53# vi: ts=4 expandtab82# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py
index 35d8c57..98b0e66 100755
--- a/cloudinit/config/cc_ssh_authkey_fingerprints.py
+++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py
@@ -77,11 +77,10 @@ def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5',
77 tbl = SimpleTable(tbl_fields)77 tbl = SimpleTable(tbl_fields)
78 for entry in key_entries:78 for entry in key_entries:
79 if _is_printable_key(entry):79 if _is_printable_key(entry):
80 row = []80 row = [entry.keytype or '-',
81 row.append(entry.keytype or '-')81 _gen_fingerprint(entry.base64, hash_meth) or '-',
82 row.append(_gen_fingerprint(entry.base64, hash_meth) or '-')82 entry.options or '-',
83 row.append(entry.options or '-')83 entry.comment or '-']
84 row.append(entry.comment or '-')
85 tbl.add_row(row)84 tbl.add_row(row)
86 authtbl_s = tbl.get_string()85 authtbl_s = tbl.get_string()
87 authtbl_lines = authtbl_s.splitlines()86 authtbl_lines = authtbl_s.splitlines()
diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py
index f87a343..b814c8b 100644
--- a/cloudinit/distros/arch.py
+++ b/cloudinit/distros/arch.py
@@ -129,11 +129,8 @@ class Distro(distros.Distro):
129 if pkgs is None:129 if pkgs is None:
130 pkgs = []130 pkgs = []
131131
132 cmd = ['pacman']132 cmd = ['pacman', "-Sy", "--quiet", "--noconfirm"]
133 # Redirect output133 # Redirect output
134 cmd.append("-Sy")
135 cmd.append("--quiet")
136 cmd.append("--noconfirm")
137134
138 if args and isinstance(args, str):135 if args and isinstance(args, str):
139 cmd.append(args)136 cmd.append(args)
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index aa468bc..754d3df 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -132,6 +132,12 @@ class Distro(distros.Distro):
132 LOG.debug("Using network interface %s", bsddev)132 LOG.debug("Using network interface %s", bsddev)
133 return bsddev133 return bsddev
134134
135 def _select_hostname(self, hostname, fqdn):
136 # Should be FQDN if available. See rc.conf(5) in FreeBSD
137 if fqdn:
138 return fqdn
139 return hostname
140
135 def _read_system_hostname(self):141 def _read_system_hostname(self):
136 sys_hostname = self._read_hostname(filename=None)142 sys_hostname = self._read_hostname(filename=None)
137 return ('rc.conf', sys_hostname)143 return ('rc.conf', sys_hostname)
diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py
index a219e9f..162dfa0 100644
--- a/cloudinit/distros/opensuse.py
+++ b/cloudinit/distros/opensuse.py
@@ -67,11 +67,10 @@ class Distro(distros.Distro):
67 if pkgs is None:67 if pkgs is None:
68 pkgs = []68 pkgs = []
6969
70 cmd = ['zypper']
71 # No user interaction possible, enable non-interactive mode70 # No user interaction possible, enable non-interactive mode
72 cmd.append('--non-interactive')71 cmd = ['zypper', '--non-interactive']
7372
74 # Comand is the operation, such as install73 # Command is the operation, such as install
75 if command == 'upgrade':74 if command == 'upgrade':
76 command = 'update'75 command = 'update'
77 cmd.append(command)76 cmd.append(command)
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 4bcbf3a..0bb7fad 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -223,6 +223,8 @@ DEF_PASSWD_REDACTION = 'REDACTED'
223223
224224
225def get_hostname(hostname_command='hostname'):225def get_hostname(hostname_command='hostname'):
226 if not isinstance(hostname_command, (list, tuple)):
227 hostname_command = (hostname_command,)
226 return util.subp(hostname_command, capture=True)[0].strip()228 return util.subp(hostname_command, capture=True)[0].strip()
227229
228230
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index ce47b6b..9450835 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -173,10 +173,7 @@ class OpenNebulaNetwork(object):
173 def gen_conf(self):173 def gen_conf(self):
174 global_dns = self.context.get('DNS', "").split()174 global_dns = self.context.get('DNS', "").split()
175175
176 conf = []176 conf = ['auto lo', 'iface lo inet loopback', '']
177 conf.append('auto lo')
178 conf.append('iface lo inet loopback')
179 conf.append('')
180177
181 for mac, dev in self.ifaces.items():178 for mac, dev in self.ifaces.items():
182 mac = mac.lower()179 mac = mac.lower()
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index a05ca2f..df0b374 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -276,21 +276,34 @@ class DataSource(object):
276 return "iid-datasource"276 return "iid-datasource"
277 return str(self.metadata['instance-id'])277 return str(self.metadata['instance-id'])
278278
279 def get_hostname(self, fqdn=False, resolve_ip=False):279 def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False):
280 """Get hostname or fqdn from the datasource. Look it up if desired.
281
282 @param fqdn: Boolean, set True to return hostname with domain.
283 @param resolve_ip: Boolean, set True to attempt to resolve an ipv4
284 address provided in local-hostname meta-data.
285 @param metadata_only: Boolean, set True to avoid looking up hostname
286 if meta-data doesn't have local-hostname present.
287
288 @return: hostname or qualified hostname. Optionally return None when
289 metadata_only is True and local-hostname data is not available.
290 """
280 defdomain = "localdomain"291 defdomain = "localdomain"
281 defhost = "localhost"292 defhost = "localhost"
282 domain = defdomain293 domain = defdomain
283294
284 if not self.metadata or 'local-hostname' not in self.metadata:295 if not self.metadata or 'local-hostname' not in self.metadata:
296 if metadata_only:
297 return None
285 # this is somewhat questionable really.298 # this is somewhat questionable really.
286 # the cloud datasource was asked for a hostname299 # the cloud datasource was asked for a hostname
287 # and didn't have one. raising error might be more appropriate300 # and didn't have one. raising error might be more appropriate
288 # but instead, basically look up the existing hostname301 # but instead, basically look up the existing hostname
289 toks = []302 toks = []
290 hostname = util.get_hostname()303 hostname = util.get_hostname()
291 fqdn = util.get_fqdn_from_hosts(hostname)304 hosts_fqdn = util.get_fqdn_from_hosts(hostname)
292 if fqdn and fqdn.find(".") > 0:305 if hosts_fqdn and hosts_fqdn.find(".") > 0:
293 toks = str(fqdn).split(".")306 toks = str(hosts_fqdn).split(".")
294 elif hostname and hostname.find(".") > 0:307 elif hostname and hostname.find(".") > 0:
295 toks = str(hostname).split(".")308 toks = str(hostname).split(".")
296 elif hostname:309 elif hostname:
diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
index af15115..5065083 100644
--- a/cloudinit/sources/tests/test_init.py
+++ b/cloudinit/sources/tests/test_init.py
@@ -7,7 +7,7 @@ import stat
7from cloudinit.helpers import Paths7from cloudinit.helpers import Paths
8from cloudinit.sources import (8from cloudinit.sources import (
9 INSTANCE_JSON_FILE, DataSource)9 INSTANCE_JSON_FILE, DataSource)
10from cloudinit.tests.helpers import CiTestCase, skipIf10from cloudinit.tests.helpers import CiTestCase, skipIf, mock
11from cloudinit.user_data import UserDataProcessor11from cloudinit.user_data import UserDataProcessor
12from cloudinit import util12from cloudinit import util
1313
@@ -108,6 +108,74 @@ class TestDataSource(CiTestCase):
108 self.assertEqual('userdata_raw', datasource.userdata_raw)108 self.assertEqual('userdata_raw', datasource.userdata_raw)
109 self.assertEqual('vendordata_raw', datasource.vendordata_raw)109 self.assertEqual('vendordata_raw', datasource.vendordata_raw)
110110
111 def test_get_hostname_strips_local_hostname_without_domain(self):
112 """Datasource.get_hostname strips metadata local-hostname of domain."""
113 tmp = self.tmp_dir()
114 datasource = DataSourceTestSubclassNet(
115 self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
116 self.assertTrue(datasource.get_data())
117 self.assertEqual(
118 'test-subclass-hostname', datasource.metadata['local-hostname'])
119 self.assertEqual('test-subclass-hostname', datasource.get_hostname())
120 datasource.metadata['local-hostname'] = 'hostname.my.domain.com'
121 self.assertEqual('hostname', datasource.get_hostname())
122
123 def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self):
124 """Datasource.get_hostname with fqdn set gets qualified hostname."""
125 tmp = self.tmp_dir()
126 datasource = DataSourceTestSubclassNet(
127 self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
128 self.assertTrue(datasource.get_data())
129 datasource.metadata['local-hostname'] = 'hostname.my.domain.com'
130 self.assertEqual(
131 'hostname.my.domain.com', datasource.get_hostname(fqdn=True))
132
133 def test_get_hostname_without_metadata_uses_system_hostname(self):
134 """Datasource.gethostname runs util.get_hostname when no metadata."""
135 tmp = self.tmp_dir()
136 datasource = DataSourceTestSubclassNet(
137 self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
138 self.assertEqual({}, datasource.metadata)
139 mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
140 with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
141 with mock.patch(mock_fqdn) as m_fqdn:
142 m_gethost.return_value = 'systemhostname.domain.com'
143 m_fqdn.return_value = None # No maching fqdn in /etc/hosts
144 self.assertEqual('systemhostname', datasource.get_hostname())
145 self.assertEqual(
146 'systemhostname.domain.com',
147 datasource.get_hostname(fqdn=True))
148
149 def test_get_hostname_without_metadata_returns_none(self):
150 """Datasource.gethostname returns None when metadata_only and no MD."""
151 tmp = self.tmp_dir()
152 datasource = DataSourceTestSubclassNet(
153 self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
154 self.assertEqual({}, datasource.metadata)
155 mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
156 with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
157 with mock.patch(mock_fqdn) as m_fqdn:
158 self.assertIsNone(datasource.get_hostname(metadata_only=True))
159 self.assertIsNone(
160 datasource.get_hostname(fqdn=True, metadata_only=True))
161 self.assertEqual([], m_gethost.call_args_list)
162 self.assertEqual([], m_fqdn.call_args_list)
163
164 def test_get_hostname_without_metadata_prefers_etc_hosts(self):
165 """Datasource.gethostname prefers /etc/hosts to util.get_hostname."""
166 tmp = self.tmp_dir()
167 datasource = DataSourceTestSubclassNet(
168 self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
169 self.assertEqual({}, datasource.metadata)
170 mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
171 with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
172 with mock.patch(mock_fqdn) as m_fqdn:
173 m_gethost.return_value = 'systemhostname.domain.com'
174 m_fqdn.return_value = 'fqdnhostname.domain.com'
175 self.assertEqual('fqdnhostname', datasource.get_hostname())
176 self.assertEqual('fqdnhostname.domain.com',
177 datasource.get_hostname(fqdn=True))
178
111 def test_get_data_write_json_instance_data(self):179 def test_get_data_write_json_instance_data(self):
112 """get_data writes INSTANCE_JSON_FILE to run_dir as readonly root."""180 """get_data writes INSTANCE_JSON_FILE to run_dir as readonly root."""
113 tmp = self.tmp_dir()181 tmp = self.tmp_dir()
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index d045268..bc4ebc8 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -132,8 +132,7 @@ class Init(object):
132 return initial_dirs132 return initial_dirs
133133
134 def purge_cache(self, rm_instance_lnk=False):134 def purge_cache(self, rm_instance_lnk=False):
135 rm_list = []135 rm_list = [self.paths.boot_finished]
136 rm_list.append(self.paths.boot_finished)
137 if rm_instance_lnk:136 if rm_instance_lnk:
138 rm_list.append(self.paths.instance_link)137 rm_list.append(self.paths.instance_link)
139 for f in rm_list:138 for f in rm_list:
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 41d9a8e..14c0b0b 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -409,6 +409,19 @@ except AttributeError:
409 return decorator409 return decorator
410410
411411
412try:
413 import jsonschema
414 assert jsonschema # avoid pyflakes error F401: import unused
415 _missing_jsonschema_dep = False
416except ImportError:
417 _missing_jsonschema_dep = True
418
419
420def skipUnlessJsonSchema():
421 return skipIf(
422 _missing_jsonschema_dep, "No python-jsonschema dependency present.")
423
424
412# older versions of mock do not have the useful 'assert_not_called'425# older versions of mock do not have the useful 'assert_not_called'
413if not hasattr(mock.Mock, 'assert_not_called'):426if not hasattr(mock.Mock, 'assert_not_called'):
414 def __mock_assert_not_called(mmock):427 def __mock_assert_not_called(mmock):
diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
index ba6bf69..d30643d 100644
--- a/cloudinit/tests/test_util.py
+++ b/cloudinit/tests/test_util.py
@@ -16,6 +16,25 @@ MOUNT_INFO = [
16]16]
1717
1818
19class FakeCloud(object):
20
21 def __init__(self, hostname, fqdn):
22 self.hostname = hostname
23 self.fqdn = fqdn
24 self.calls = []
25
26 def get_hostname(self, fqdn=None, metadata_only=None):
27 myargs = {}
28 if fqdn is not None:
29 myargs['fqdn'] = fqdn
30 if metadata_only is not None:
31 myargs['metadata_only'] = metadata_only
32 self.calls.append(myargs)
33 if fqdn:
34 return self.fqdn
35 return self.hostname
36
37
19class TestUtil(CiTestCase):38class TestUtil(CiTestCase):
2039
21 def test_parse_mount_info_no_opts_no_arg(self):40 def test_parse_mount_info_no_opts_no_arg(self):
@@ -44,3 +63,81 @@ class TestUtil(CiTestCase):
44 m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime')63 m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime')
45 is_rw = util.mount_is_read_write('/')64 is_rw = util.mount_is_read_write('/')
46 self.assertEqual(is_rw, False)65 self.assertEqual(is_rw, False)
66
67
68class TestShellify(CiTestCase):
69
70 def test_input_dict_raises_type_error(self):
71 self.assertRaisesRegex(
72 TypeError, 'Input.*was.*dict.*xpected',
73 util.shellify, {'mykey': 'myval'})
74
75 def test_input_str_raises_type_error(self):
76 self.assertRaisesRegex(
77 TypeError, 'Input.*was.*str.*xpected', util.shellify, "foobar")
78
79 def test_value_with_int_raises_type_error(self):
80 self.assertRaisesRegex(
81 TypeError, 'shellify.*int', util.shellify, ["foo", 1])
82
83 def test_supports_strings_and_lists(self):
84 self.assertEqual(
85 '\n'.join(["#!/bin/sh", "echo hi mom", "'echo' 'hi dad'",
86 "'echo' 'hi' 'sis'", ""]),
87 util.shellify(["echo hi mom", ["echo", "hi dad"],
88 ('echo', 'hi', 'sis')]))
89
90
91class TestGetHostnameFqdn(CiTestCase):
92
93 def test_get_hostname_fqdn_from_only_cfg_fqdn(self):
94 """When cfg only has the fqdn key, derive hostname and fqdn from it."""
95 hostname, fqdn = util.get_hostname_fqdn(
96 cfg={'fqdn': 'myhost.domain.com'}, cloud=None)
97 self.assertEqual('myhost', hostname)
98 self.assertEqual('myhost.domain.com', fqdn)
99
100 def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self):
101 """When cfg has both fqdn and hostname keys, return them."""
102 hostname, fqdn = util.get_hostname_fqdn(
103 cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None)
104 self.assertEqual('other', hostname)
105 self.assertEqual('myhost.domain.com', fqdn)
106
107 def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self):
108 """When cfg has only hostname key which represents a fqdn, use that."""
109 hostname, fqdn = util.get_hostname_fqdn(
110 cfg={'hostname': 'myhost.domain.com'}, cloud=None)
111 self.assertEqual('myhost', hostname)
112 self.assertEqual('myhost.domain.com', fqdn)
113
114 def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self):
115 """When cfg has a hostname without a '.' query cloud.get_hostname."""
116 mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
117 hostname, fqdn = util.get_hostname_fqdn(
118 cfg={'hostname': 'myhost'}, cloud=mycloud)
119 self.assertEqual('myhost', hostname)
120 self.assertEqual('cloudhost.mycloud.com', fqdn)
121 self.assertEqual(
122 [{'fqdn': True, 'metadata_only': False}], mycloud.calls)
123
124 def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self):
125 """When cfg has neither hostname nor fqdn cloud.get_hostname."""
126 mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
127 hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud)
128 self.assertEqual('cloudhost', hostname)
129 self.assertEqual('cloudhost.mycloud.com', fqdn)
130 self.assertEqual(
131 [{'fqdn': True, 'metadata_only': False},
132 {'metadata_only': False}], mycloud.calls)
133
134 def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self):
135 """Calls to cloud.get_hostname pass the metadata_only parameter."""
136 mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
137 hostname, fqdn = util.get_hostname_fqdn(
138 cfg={}, cloud=mycloud, metadata_only=True)
139 self.assertEqual(
140 [{'fqdn': True, 'metadata_only': True},
141 {'metadata_only': True}], mycloud.calls)
142
143# vi: ts=4 expandtab
diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py
index 0a5be0b..4e814a5 100644
--- a/cloudinit/url_helper.py
+++ b/cloudinit/url_helper.py
@@ -47,7 +47,7 @@ try:
47 _REQ_VER = LooseVersion(_REQ.version) # pylint: disable=no-member47 _REQ_VER = LooseVersion(_REQ.version) # pylint: disable=no-member
48 if _REQ_VER >= LooseVersion('0.8.8'):48 if _REQ_VER >= LooseVersion('0.8.8'):
49 SSL_ENABLED = True49 SSL_ENABLED = True
50 if _REQ_VER >= LooseVersion('0.7.0') and _REQ_VER < LooseVersion('1.0.0'):50 if LooseVersion('0.7.0') <= _REQ_VER < LooseVersion('1.0.0'):
51 CONFIG_ENABLED = True51 CONFIG_ENABLED = True
52except ImportError:52except ImportError:
53 pass53 pass
@@ -121,7 +121,7 @@ class UrlResponse(object):
121 upper = 300121 upper = 300
122 if redirects_ok:122 if redirects_ok:
123 upper = 400123 upper = 400
124 if self.code >= 200 and self.code < upper:124 if 200 <= self.code < upper:
125 return True125 return True
126 else:126 else:
127 return False127 return False
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 02dc2ce..823d80b 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -546,7 +546,7 @@ def is_ipv4(instr):
546 return False546 return False
547547
548 try:548 try:
549 toks = [x for x in toks if int(x) < 256 and int(x) >= 0]549 toks = [x for x in toks if 0 <= int(x) < 256]
550 except Exception:550 except Exception:
551 return False551 return False
552552
@@ -716,8 +716,7 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
716def make_url(scheme, host, port=None,716def make_url(scheme, host, port=None,
717 path='', params='', query='', fragment=''):717 path='', params='', query='', fragment=''):
718718
719 pieces = []719 pieces = [scheme or '']
720 pieces.append(scheme or '')
721720
722 netloc = ''721 netloc = ''
723 if host:722 if host:
@@ -1026,9 +1025,16 @@ def dos2unix(contents):
1026 return contents.replace('\r\n', '\n')1025 return contents.replace('\r\n', '\n')
10271026
10281027
1029def get_hostname_fqdn(cfg, cloud):1028def get_hostname_fqdn(cfg, cloud, metadata_only=False):
1030 # return the hostname and fqdn from 'cfg'. If not found in cfg,1029 """Get hostname and fqdn from config if present and fallback to cloud.
1031 # then fall back to data from cloud1030
1031 @param cfg: Dictionary of merged user-data configuration (from init.cfg).
1032 @param cloud: Cloud instance from init.cloudify().
1033 @param metadata_only: Boolean, set True to only query cloud meta-data,
1034 returning None if not present in meta-data.
1035 @return: a Tuple of strings <hostname>, <fqdn>. Values can be none when
1036 metadata_only is True and no cfg or metadata provides hostname info.
1037 """
1032 if "fqdn" in cfg:1038 if "fqdn" in cfg:
1033 # user specified a fqdn. Default hostname then is based off that1039 # user specified a fqdn. Default hostname then is based off that
1034 fqdn = cfg['fqdn']1040 fqdn = cfg['fqdn']
@@ -1042,11 +1048,11 @@ def get_hostname_fqdn(cfg, cloud):
1042 else:1048 else:
1043 # no fqdn set, get fqdn from cloud.1049 # no fqdn set, get fqdn from cloud.
1044 # get hostname from cfg if available otherwise cloud1050 # get hostname from cfg if available otherwise cloud
1045 fqdn = cloud.get_hostname(fqdn=True)1051 fqdn = cloud.get_hostname(fqdn=True, metadata_only=metadata_only)
1046 if "hostname" in cfg:1052 if "hostname" in cfg:
1047 hostname = cfg['hostname']1053 hostname = cfg['hostname']
1048 else:1054 else:
1049 hostname = cloud.get_hostname()1055 hostname = cloud.get_hostname(metadata_only=metadata_only)
1050 return (hostname, fqdn)1056 return (hostname, fqdn)
10511057
10521058
@@ -1868,8 +1874,14 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1868 # Popen converts entries in the arguments array from non-bytes to bytes.1874 # Popen converts entries in the arguments array from non-bytes to bytes.
1869 # When locale is unset it may use ascii for that encoding which can1875 # When locale is unset it may use ascii for that encoding which can
1870 # cause UnicodeDecodeErrors. (LP: #1751051)1876 # cause UnicodeDecodeErrors. (LP: #1751051)
1871 bytes_args = [x if isinstance(x, six.binary_type) else x.encode("utf-8")1877 if isinstance(args, six.binary_type):
1872 for x in args]1878 bytes_args = args
1879 elif isinstance(args, six.string_types):
1880 bytes_args = args.encode("utf-8")
1881 else:
1882 bytes_args = [
1883 x if isinstance(x, six.binary_type) else x.encode("utf-8")
1884 for x in args]
1873 try:1885 try:
1874 sp = subprocess.Popen(bytes_args, stdout=stdout,1886 sp = subprocess.Popen(bytes_args, stdout=stdout,
1875 stderr=stderr, stdin=stdin,1887 stderr=stderr, stdin=stdin,
@@ -1923,6 +1935,11 @@ def abs_join(*paths):
1923# if it is an array, shell protect it (with single ticks)1935# if it is an array, shell protect it (with single ticks)
1924# if it is a string, do nothing1936# if it is a string, do nothing
1925def shellify(cmdlist, add_header=True):1937def shellify(cmdlist, add_header=True):
1938 if not isinstance(cmdlist, (tuple, list)):
1939 raise TypeError(
1940 "Input to shellify was type '%s'. Expected list or tuple." %
1941 (type_utils.obj_name(cmdlist)))
1942
1926 content = ''1943 content = ''
1927 if add_header:1944 if add_header:
1928 content += "#!/bin/sh\n"1945 content += "#!/bin/sh\n"
@@ -1931,7 +1948,7 @@ def shellify(cmdlist, add_header=True):
1931 for args in cmdlist:1948 for args in cmdlist:
1932 # If the item is a list, wrap all items in single tick.1949 # If the item is a list, wrap all items in single tick.
1933 # If its not, then just write it directly.1950 # If its not, then just write it directly.
1934 if isinstance(args, list):1951 if isinstance(args, (list, tuple)):
1935 fixed = []1952 fixed = []
1936 for f in args:1953 for f in args:
1937 fixed.append("'%s'" % (six.text_type(f).replace("'", escaped)))1954 fixed.append("'%s'" % (six.text_type(f).replace("'", escaped)))
@@ -1941,9 +1958,10 @@ def shellify(cmdlist, add_header=True):
1941 content = "%s%s\n" % (content, args)1958 content = "%s%s\n" % (content, args)
1942 cmds_made += 11959 cmds_made += 1
1943 else:1960 else:
1944 raise RuntimeError(("Unable to shellify type %s"1961 raise TypeError(
1945 " which is not a list or string")1962 "Unable to shellify type '%s'. Expected list, string, tuple. "
1946 % (type_utils.obj_name(args)))1963 "Got: %s" % (type_utils.obj_name(args), args))
1964
1947 LOG.debug("Shellified %s commands.", cmds_made)1965 LOG.debug("Shellified %s commands.", cmds_made)
1948 return content1966 return content
19491967
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index fad1184..cf2e240 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -113,9 +113,9 @@ cloud_final_modules:
113{% if variant not in ["freebsd"] %}113{% if variant not in ["freebsd"] %}
114 - puppet114 - puppet
115 - chef115 - chef
116 - salt-minion
117 - mcollective116 - mcollective
118{% endif %}117{% endif %}
118 - salt-minion
119 - rightscale_userdata119 - rightscale_userdata
120 - scripts-vendor120 - scripts-vendor
121 - scripts-per-once121 - scripts-per-once
diff --git a/debian/changelog b/debian/changelog
index 27dba2c..f1ba6ef 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,25 @@
1cloud-init (18.1-17-g97012fbb-0ubuntu1) bionic; urgency=medium
2
3 * New upstream snapshot.
4 - util: Fix subp regression. Allow specifying subp command as a string.
5 (LP: #1755965)
6 - doc: fix all warnings issued by 'tox -e doc'
7 - FreeBSD: Set hostname to FQDN. [Dominic Schlegel] (LP: #1753499)
8 - tests: fix run_tree and bddeb
9 - tests: Fix some warnings in tests that popped up with newer python.
10 - set_hostname: When present in metadata, set it before network bringup.
11 (LP: #1746455)
12 - tests: Centralize and re-use skipTest based on json schema presense.
13 - This commit fixes get_hostname on the AzureDataSource.
14 [Douglas Jordan] (LP: #1754495)
15 - shellify: raise TypeError on bad input.
16 - Make salt minion module work on FreeBSD.
17 [Dominic Schlegel] (LP: #1721503)
18 - Simplify some comparisions. [Rémy Léone]
19 - Change some list creation and population to literal. [Rémy Léone]
20
21 -- Chad Smith <chad.smith@canonical.com> Thu, 15 Mar 2018 14:48:29 -0600
22
1cloud-init (18.1-5-g40e77380-0ubuntu1) bionic; urgency=medium23cloud-init (18.1-5-g40e77380-0ubuntu1) bionic; urgency=medium
224
3 * New upstream snapshot.25 * New upstream snapshot.
diff --git a/doc/rtd/topics/capabilities.rst b/doc/rtd/topics/capabilities.rst
index ae3a0c7..3e2c9e3 100644
--- a/doc/rtd/topics/capabilities.rst
+++ b/doc/rtd/topics/capabilities.rst
@@ -44,13 +44,14 @@ Currently defined feature names include:
44CLI Interface44CLI Interface
45=============45=============
4646
47 The command line documentation is accessible on any cloud-init47The command line documentation is accessible on any cloud-init installed
48installed system:48system:
4949
50.. code-block:: bash50.. code-block:: shell-session
5151
52 % cloud-init --help52 % cloud-init --help
53 usage: cloud-init [-h] [--version] [--file FILES]53 usage: cloud-init [-h] [--version] [--file FILES]
54
54 [--debug] [--force]55 [--debug] [--force]
55 {init,modules,single,dhclient-hook,features,analyze,devel,collect-logs,clean,status}56 {init,modules,single,dhclient-hook,features,analyze,devel,collect-logs,clean,status}
56 ...57 ...
@@ -88,7 +89,7 @@ Print out each feature supported. If cloud-init does not have the
88features subcommand, it also does not support any features described in89features subcommand, it also does not support any features described in
89this document.90this document.
9091
91.. code-block:: bash92.. code-block:: shell-session
9293
93 % cloud-init features94 % cloud-init features
94 NETWORK_CONFIG_V195 NETWORK_CONFIG_V1
@@ -100,10 +101,11 @@ cloud-init status
100-----------------101-----------------
101Report whether cloud-init is running, done, disabled or errored. Exits102Report whether cloud-init is running, done, disabled or errored. Exits
102non-zero if an error is detected in cloud-init.103non-zero if an error is detected in cloud-init.
104
103 * **--long**: Detailed status information.105 * **--long**: Detailed status information.
104 * **--wait**: Block until cloud-init completes.106 * **--wait**: Block until cloud-init completes.
105107
106.. code-block:: bash108.. code-block:: shell-session
107109
108 % cloud-init status --long110 % cloud-init status --long
109 status: done111 status: done
@@ -214,7 +216,7 @@ of once-per-instance:
214 * **--frequency**: Optionally override the declared module frequency216 * **--frequency**: Optionally override the declared module frequency
215 with one of (always|once-per-instance|once)217 with one of (always|once-per-instance|once)
216218
217.. code-block:: bash219.. code-block:: shell-session
218220
219 % cloud-init single --name set_hostname --frequency always221 % cloud-init single --name set_hostname --frequency always
220222
diff --git a/doc/rtd/topics/debugging.rst b/doc/rtd/topics/debugging.rst
index c2b47ed..cacc8a2 100644
--- a/doc/rtd/topics/debugging.rst
+++ b/doc/rtd/topics/debugging.rst
@@ -1,6 +1,6 @@
1**********************1********************************
2Testing and debugging cloud-init2Testing and debugging cloud-init
3**********************3********************************
44
5Overview5Overview
6========6========
@@ -10,7 +10,7 @@ deployed instances.
10.. _boot_time_analysis:10.. _boot_time_analysis:
1111
12Boot Time Analysis - cloud-init analyze12Boot Time Analysis - cloud-init analyze
13======================================13=======================================
14Occasionally instances don't appear as performant as we would like and14Occasionally instances don't appear as performant as we would like and
15cloud-init packages a simple facility to inspect what operations took15cloud-init packages a simple facility to inspect what operations took
16cloud-init the longest during boot and setup.16cloud-init the longest during boot and setup.
@@ -22,9 +22,9 @@ determine the long-pole in cloud-init configuration and setup. These
22subcommands default to reading /var/log/cloud-init.log.22subcommands default to reading /var/log/cloud-init.log.
2323
24* ``analyze show`` Parse and organize cloud-init.log events by stage and24* ``analyze show`` Parse and organize cloud-init.log events by stage and
25include each sub-stage granularity with time delta reports.25 include each sub-stage granularity with time delta reports.
2626
27.. code-block:: bash27.. code-block:: shell-session
2828
29 $ cloud-init analyze show -i my-cloud-init.log29 $ cloud-init analyze show -i my-cloud-init.log
30 -- Boot Record 01 --30 -- Boot Record 01 --
@@ -41,9 +41,9 @@ include each sub-stage granularity with time delta reports.
4141
4242
43* ``analyze dump`` Parse cloud-init.log into event records and return a list of43* ``analyze dump`` Parse cloud-init.log into event records and return a list of
44dictionaries that can be consumed for other reporting needs.44 dictionaries that can be consumed for other reporting needs.
4545
46.. code-block:: bash46.. code-block:: shell-session
4747
48 $ cloud-init analyze blame -i my-cloud-init.log48 $ cloud-init analyze blame -i my-cloud-init.log
49 [49 [
@@ -56,10 +56,10 @@ dictionaries that can be consumed for other reporting needs.
56 },...56 },...
5757
58* ``analyze blame`` Parse cloud-init.log into event records and sort them based58* ``analyze blame`` Parse cloud-init.log into event records and sort them based
59on highest time cost for quick assessment of areas of cloud-init that may need59 on highest time cost for quick assessment of areas of cloud-init that may
60improvement.60 need improvement.
6161
62.. code-block:: bash62.. code-block:: shell-session
6363
64 $ cloud-init analyze blame -i my-cloud-init.log64 $ cloud-init analyze blame -i my-cloud-init.log
65 -- Boot Record 11 --65 -- Boot Record 11 --
@@ -73,31 +73,36 @@ Analyze quickstart - LXC
73---------------------------73---------------------------
74To quickly obtain a cloud-init log try using lxc on any ubuntu system:74To quickly obtain a cloud-init log try using lxc on any ubuntu system:
7575
76.. code-block:: bash76.. code-block:: shell-session
77
78 $ lxc init ubuntu-daily:xenial x1
79 $ lxc start x1
80 $ # Take lxc's cloud-init.log and pipe it to the analyzer
81 $ lxc file pull x1/var/log/cloud-init.log - | cloud-init analyze dump -i -
82 $ lxc file pull x1/var/log/cloud-init.log - | \
83 python3 -m cloudinit.analyze dump -i -
7784
78 $ lxc init ubuntu-daily:xenial x1
79 $ lxc start x1
80 # Take lxc's cloud-init.log and pipe it to the analyzer
81 $ lxc file pull x1/var/log/cloud-init.log - | cloud-init analyze dump -i -
82 $ lxc file pull x1/var/log/cloud-init.log - | \
83 python3 -m cloudinit.analyze dump -i -
8485
85Analyze quickstart - KVM86Analyze quickstart - KVM
86---------------------------87---------------------------
87To quickly analyze a KVM a cloud-init log:88To quickly analyze a KVM a cloud-init log:
8889
891. Download the current cloud image901. Download the current cloud image
90 wget https://cloud-images.ubuntu.com/daily/server/xenial/current/xenial-server-cloudimg-amd64.img91
92.. code-block:: shell-session
93
94 $ wget https://cloud-images.ubuntu.com/daily/server/xenial/current/xenial-server-cloudimg-amd64.img
95
912. Create a snapshot image to preserve the original cloud-image962. Create a snapshot image to preserve the original cloud-image
9297
93.. code-block:: bash98.. code-block:: shell-session
9499
95 $ qemu-img create -b xenial-server-cloudimg-amd64.img -f qcow2 \100 $ qemu-img create -b xenial-server-cloudimg-amd64.img -f qcow2 \
96 test-cloudinit.qcow2101 test-cloudinit.qcow2
97102
983. Create a seed image with metadata using `cloud-localds`1033. Create a seed image with metadata using `cloud-localds`
99104
100.. code-block:: bash105.. code-block:: shell-session
101106
102 $ cat > user-data <<EOF107 $ cat > user-data <<EOF
103 #cloud-config108 #cloud-config
@@ -108,18 +113,18 @@ To quickly analyze a KVM a cloud-init log:
108113
1094. Launch your modified VM1144. Launch your modified VM
110115
111.. code-block:: bash116.. code-block:: shell-session
112117
113 $ kvm -m 512 -net nic -net user -redir tcp:2222::22 \118 $ kvm -m 512 -net nic -net user -redir tcp:2222::22 \
114 -drive file=test-cloudinit.qcow2,if=virtio,format=qcow2 \119 -drive file=test-cloudinit.qcow2,if=virtio,format=qcow2 \
115 -drive file=my-seed.img,if=virtio,format=raw120 -drive file=my-seed.img,if=virtio,format=raw
116121
1175. Analyze the boot (blame, dump, show)1225. Analyze the boot (blame, dump, show)
118123
119.. code-block:: bash124.. code-block:: shell-session
120125
121 $ ssh -p 2222 ubuntu@localhost 'cat /var/log/cloud-init.log' | \126 $ ssh -p 2222 ubuntu@localhost 'cat /var/log/cloud-init.log' | \
122 cloud-init analyze blame -i -127 cloud-init analyze blame -i -
123128
124129
125Running single cloud config modules130Running single cloud config modules
@@ -136,7 +141,7 @@ prevents a module from running again if it has already been run. To ensure that
136a module is run again, the desired frequency can be overridden on the141a module is run again, the desired frequency can be overridden on the
137commandline:142commandline:
138143
139.. code-block:: bash144.. code-block:: shell-session
140145
141 $ sudo cloud-init single --name cc_ssh --frequency always146 $ sudo cloud-init single --name cc_ssh --frequency always
142 ...147 ...
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 96c1cf5..1e99455 100644
--- a/doc/rtd/topics/network-config.rst
+++ b/doc/rtd/topics/network-config.rst
@@ -202,7 +202,7 @@ is helpful for examining expected output for a given input format.
202202
203CLI Interface :203CLI Interface :
204204
205.. code-block:: bash205.. code-block:: shell-session
206206
207 % tools/net-convert.py --help207 % tools/net-convert.py --help
208 usage: net-convert.py [-h] --network-data PATH --kind208 usage: net-convert.py [-h] --network-data PATH --kind
@@ -222,7 +222,7 @@ CLI Interface :
222222
223Example output converting V2 to sysconfig:223Example output converting V2 to sysconfig:
224224
225.. code-block:: bash225.. code-block:: shell-session
226226
227 % tools/net-convert.py --network-data v2.yaml --kind yaml \227 % tools/net-convert.py --network-data v2.yaml --kind yaml \
228 --output-kind sysconfig -d target228 --output-kind sysconfig -d target
diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst
index bf04bb3..cac4a6e 100644
--- a/doc/rtd/topics/tests.rst
+++ b/doc/rtd/topics/tests.rst
@@ -21,7 +21,7 @@ Overview
21In order to avoid the need for dependencies and ease the setup and21In order to avoid the need for dependencies and ease the setup and
22configuration users can run the integration tests via tox:22configuration users can run the integration tests via tox:
2323
24.. code-block:: bash24.. code-block:: shell-session
2525
26 $ git clone https://git.launchpad.net/cloud-init26 $ git clone https://git.launchpad.net/cloud-init
27 $ cd cloud-init27 $ cd cloud-init
@@ -51,7 +51,7 @@ The first example will provide a complete end-to-end run of data
51collection and verification. There are additional examples below51collection and verification. There are additional examples below
52explaining how to run one or the other independently.52explaining how to run one or the other independently.
5353
54.. code-block:: bash54.. code-block:: shell-session
5555
56 $ git clone https://git.launchpad.net/cloud-init56 $ git clone https://git.launchpad.net/cloud-init
57 $ cd cloud-init57 $ cd cloud-init
@@ -93,7 +93,7 @@ If developing tests it may be necessary to see if cloud-config works as
93expected and the correct files are pulled down. In this case only a93expected and the correct files are pulled down. In this case only a
94collect can be ran by running:94collect can be ran by running:
9595
96.. code-block:: bash96.. code-block:: shell-session
9797
98 $ tox -e citest -- collect -n xenial --data-dir /tmp/collection98 $ tox -e citest -- collect -n xenial --data-dir /tmp/collection
9999
@@ -106,7 +106,7 @@ Verify
106When developing tests it is much easier to simply rerun the verify scripts106When developing tests it is much easier to simply rerun the verify scripts
107without the more lengthy collect process. This can be done by running:107without the more lengthy collect process. This can be done by running:
108108
109.. code-block:: bash109.. code-block:: shell-session
110110
111 $ tox -e citest -- verify --data-dir /tmp/collection111 $ tox -e citest -- verify --data-dir /tmp/collection
112112
@@ -133,7 +133,7 @@ cloud-init deb from or use the ``tree_run`` command using a copy of
133cloud-init located in a different directory, use the option ``--cloud-init133cloud-init located in a different directory, use the option ``--cloud-init
134/path/to/cloud-init``.134/path/to/cloud-init``.
135135
136.. code-block:: bash136.. code-block:: shell-session
137137
138 $ tox -e citest -- tree_run --verbose \138 $ tox -e citest -- tree_run --verbose \
139 --os-name xenial --os-name stretch \139 --os-name xenial --os-name stretch \
@@ -331,7 +331,7 @@ Integration tests are located under the `tests/cloud_tests` directory.
331Test configurations are placed under `configs` and the test verification331Test configurations are placed under `configs` and the test verification
332scripts under `testcases`:332scripts under `testcases`:
333333
334.. code-block:: bash334.. code-block:: shell-session
335335
336 cloud-init$ tree -d tests/cloud_tests/336 cloud-init$ tree -d tests/cloud_tests/
337 tests/cloud_tests/337 tests/cloud_tests/
@@ -362,7 +362,7 @@ The following would create a test case named ``example`` under the
362``modules`` category with the given description, and cloud config data read362``modules`` category with the given description, and cloud config data read
363in from ``/tmp/user_data``.363in from ``/tmp/user_data``.
364364
365.. code-block:: bash365.. code-block:: shell-session
366366
367 $ tox -e citest -- create modules/example \367 $ tox -e citest -- create modules/example \
368 -d "a simple example test case" -c "$(< /tmp/user_data)"368 -d "a simple example test case" -c "$(< /tmp/user_data)"
@@ -385,7 +385,7 @@ Development Checklist
385 * Placed in the appropriate sub-folder in the test cases directory385 * Placed in the appropriate sub-folder in the test cases directory
386* Tested by running the test:386* Tested by running the test:
387387
388 .. code-block:: bash388 .. code-block:: shell-session
389389
390 $ tox -e citest -- run -verbose \390 $ tox -e citest -- run -verbose \
391 --os-name <release target> \391 --os-name <release target> \
@@ -404,14 +404,14 @@ These configuration files are the standard that the AWS cli and other AWS
404tools utilize for interacting directly with AWS itself and are normally404tools utilize for interacting directly with AWS itself and are normally
405generated when running ``aws configure``:405generated when running ``aws configure``:
406406
407.. code-block:: bash407.. code-block:: shell-session
408408
409 $ cat $HOME/.aws/credentials409 $ cat $HOME/.aws/credentials
410 [default]410 [default]
411 aws_access_key_id = <KEY HERE>411 aws_access_key_id = <KEY HERE>
412 aws_secret_access_key = <KEY HERE>412 aws_secret_access_key = <KEY HERE>
413413
414.. code-block:: bash414.. code-block:: shell-session
415415
416 $ cat $HOME/.aws/config416 $ cat $HOME/.aws/config
417 [default]417 [default]
diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py
index a6d5069..b9cfcfa 100644
--- a/tests/cloud_tests/bddeb.py
+++ b/tests/cloud_tests/bddeb.py
@@ -16,7 +16,7 @@ pre_reqs = ['devscripts', 'equivs', 'git', 'tar']
1616
17def _out(cmd_res):17def _out(cmd_res):
18 """Get clean output from cmd result."""18 """Get clean output from cmd result."""
19 return cmd_res[0].strip()19 return cmd_res[0].decode("utf-8").strip()
2020
2121
22def build_deb(args, instance):22def build_deb(args, instance):
diff --git a/tests/cloud_tests/platforms/ec2/__init__.py b/tests/cloud_tests/platforms/ec2/__init__.py
23new file mode 10064423new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/cloud_tests/platforms/ec2/__init__.py
diff --git a/tests/cloud_tests/platforms/lxd/__init__.py b/tests/cloud_tests/platforms/lxd/__init__.py
24new file mode 10064424new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/cloud_tests/platforms/lxd/__init__.py
diff --git a/tests/cloud_tests/platforms/lxd/platform.py b/tests/cloud_tests/platforms/lxd/platform.py
index 6a01692..f7251a0 100644
--- a/tests/cloud_tests/platforms/lxd/platform.py
+++ b/tests/cloud_tests/platforms/lxd/platform.py
@@ -101,8 +101,4 @@ class LXDPlatform(Platform):
101 """101 """
102 return self.client.images.get_by_alias(alias)102 return self.client.images.get_by_alias(alias)
103103
104 def destroy(self):
105 """Clean up platform data."""
106 super(LXDPlatform, self).destroy()
107
108# vi: ts=4 expandtab104# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/platforms/nocloudkvm/__init__.py b/tests/cloud_tests/platforms/nocloudkvm/__init__.py
109new file mode 100644105new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/cloud_tests/platforms/nocloudkvm/__init__.py
diff --git a/tests/cloud_tests/platforms/nocloudkvm/instance.py b/tests/cloud_tests/platforms/nocloudkvm/instance.py
index 932dc0f..33ff3f2 100644
--- a/tests/cloud_tests/platforms/nocloudkvm/instance.py
+++ b/tests/cloud_tests/platforms/nocloudkvm/instance.py
@@ -109,7 +109,7 @@ class NoCloudKVMInstance(Instance):
109 if self.pid:109 if self.pid:
110 try:110 try:
111 c_util.subp(['kill', '-9', self.pid])111 c_util.subp(['kill', '-9', self.pid])
112 except util.ProcessExectuionError:112 except c_util.ProcessExecutionError:
113 pass113 pass
114114
115 if self.pid_file:115 if self.pid_file:
diff --git a/tests/cloud_tests/platforms/nocloudkvm/platform.py b/tests/cloud_tests/platforms/nocloudkvm/platform.py
index a7e6f5d..8593346 100644
--- a/tests/cloud_tests/platforms/nocloudkvm/platform.py
+++ b/tests/cloud_tests/platforms/nocloudkvm/platform.py
@@ -21,10 +21,6 @@ class NoCloudKVMPlatform(Platform):
2121
22 platform_name = 'nocloud-kvm'22 platform_name = 'nocloud-kvm'
2323
24 def __init__(self, config):
25 """Set up platform."""
26 super(NoCloudKVMPlatform, self).__init__(config)
27
28 def get_image(self, img_conf):24 def get_image(self, img_conf):
29 """Get image using specified image configuration.25 """Get image using specified image configuration.
3026
diff --git a/tests/cloud_tests/platforms/platforms.py b/tests/cloud_tests/platforms/platforms.py
index 1542b3b..abbfebb 100644
--- a/tests/cloud_tests/platforms/platforms.py
+++ b/tests/cloud_tests/platforms/platforms.py
@@ -2,12 +2,15 @@
22
3"""Base platform class."""3"""Base platform class."""
4import os4import os
5import shutil
56
6from simplestreams import filters, mirrors7from simplestreams import filters, mirrors
7from simplestreams import util as s_util8from simplestreams import util as s_util
89
9from cloudinit import util as c_util10from cloudinit import util as c_util
1011
12from tests.cloud_tests import util
13
1114
12class Platform(object):15class Platform(object):
13 """Base class for platforms."""16 """Base class for platforms."""
@@ -17,7 +20,14 @@ class Platform(object):
17 def __init__(self, config):20 def __init__(self, config):
18 """Set up platform."""21 """Set up platform."""
19 self.config = config22 self.config = config
20 self._generate_ssh_keys(config['data_dir'])23 self.tmpdir = util.mkdtemp()
24 if 'data_dir' in config:
25 self.data_dir = config['data_dir']
26 else:
27 self.data_dir = os.path.join(self.tmpdir, "data_dir")
28 os.mkdir(self.data_dir)
29
30 self._generate_ssh_keys(self.data_dir)
2131
22 def get_image(self, img_conf):32 def get_image(self, img_conf):
23 """Get image using specified image configuration.33 """Get image using specified image configuration.
@@ -29,7 +39,7 @@ class Platform(object):
2939
30 def destroy(self):40 def destroy(self):
31 """Clean up platform data."""41 """Clean up platform data."""
32 pass42 shutil.rmtree(self.tmpdir)
3343
34 def _generate_ssh_keys(self, data_dir):44 def _generate_ssh_keys(self, data_dir):
35 """Generate SSH keys to be used with image."""45 """Generate SSH keys to be used with image."""
diff --git a/tests/cloud_tests/testcases/modules/salt_minion.py b/tests/cloud_tests/testcases/modules/salt_minion.py
index f13b48a..70917a4 100644
--- a/tests/cloud_tests/testcases/modules/salt_minion.py
+++ b/tests/cloud_tests/testcases/modules/salt_minion.py
@@ -31,4 +31,9 @@ class Test(base.CloudTestCase):
31 out = self.get_data_file('grains')31 out = self.get_data_file('grains')
32 self.assertIn('role: web', out)32 self.assertIn('role: web', out)
3333
34 def test_minion_installed(self):
35 """Test if the salt-minion package is installed"""
36 out = self.get_data_file('minion_installed')
37 self.assertEqual(1, int(out))
38
34# vi: ts=4 expandtab39# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/salt_minion.yaml b/tests/cloud_tests/testcases/modules/salt_minion.yaml
index ab0e05b..f20b976 100644
--- a/tests/cloud_tests/testcases/modules/salt_minion.yaml
+++ b/tests/cloud_tests/testcases/modules/salt_minion.yaml
@@ -3,7 +3,7 @@
3#3#
4# 2016-11-17: Currently takes >60 seconds results in test failure4# 2016-11-17: Currently takes >60 seconds results in test failure
5#5#
6enabled: False6enabled: True
7cloud_config: |7cloud_config: |
8 #cloud-config8 #cloud-config
9 salt_minion:9 salt_minion:
@@ -35,5 +35,8 @@ collect_scripts:
35 grains: |35 grains: |
36 #!/bin/bash36 #!/bin/bash
37 cat /etc/salt/grains37 cat /etc/salt/grains
38 minion_installed: |
39 #!/bin/bash
40 dpkg -l | grep salt-minion | grep ii | wc -l
3841
39# vi: ts=4 expandtab42# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
index 6ff285e..3dd4996 100644
--- a/tests/cloud_tests/util.py
+++ b/tests/cloud_tests/util.py
@@ -460,6 +460,10 @@ class PlatformError(IOError):
460 IOError.__init__(self, message)460 IOError.__init__(self, message)
461461
462462
463def mkdtemp(prefix='cloud_test_data'):
464 return tempfile.mkdtemp(prefix=prefix)
465
466
463class TempDir(object):467class TempDir(object):
464 """Configurable temporary directory like tempfile.TemporaryDirectory."""468 """Configurable temporary directory like tempfile.TemporaryDirectory."""
465469
@@ -480,7 +484,7 @@ class TempDir(object):
480 @return_value: tempdir path484 @return_value: tempdir path
481 """485 """
482 if not self.tmpdir:486 if not self.tmpdir:
483 self.tmpdir = tempfile.mkdtemp(prefix=self.prefix)487 self.tmpdir = mkdtemp(prefix=self.prefix)
484 LOG.debug('using tmpdir: %s', self.tmpdir)488 LOG.debug('using tmpdir: %s', self.tmpdir)
485 return self.tmpdir489 return self.tmpdir
486490
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 254e987..da7da0c 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -643,6 +643,21 @@ fdescfs /dev/fd fdescfs rw 0 0
643 expected_config['config'].append(blacklist_config)643 expected_config['config'].append(blacklist_config)
644 self.assertEqual(netconfig, expected_config)644 self.assertEqual(netconfig, expected_config)
645645
646 @mock.patch("cloudinit.sources.DataSourceAzure.util.subp")
647 def test_get_hostname_with_no_args(self, subp):
648 dsaz.get_hostname()
649 subp.assert_called_once_with(("hostname",), capture=True)
650
651 @mock.patch("cloudinit.sources.DataSourceAzure.util.subp")
652 def test_get_hostname_with_string_arg(self, subp):
653 dsaz.get_hostname(hostname_command="hostname")
654 subp.assert_called_once_with(("hostname",), capture=True)
655
656 @mock.patch("cloudinit.sources.DataSourceAzure.util.subp")
657 def test_get_hostname_with_iterable_arg(self, subp):
658 dsaz.get_hostname(hostname_command=("hostname",))
659 subp.assert_called_once_with(("hostname",), capture=True)
660
646661
647class TestAzureBounce(CiTestCase):662class TestAzureBounce(CiTestCase):
648663
diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/test_handler/test_handler_bootcmd.py
index dbf43e0..29fc25e 100644
--- a/tests/unittests/test_handler/test_handler_bootcmd.py
+++ b/tests/unittests/test_handler/test_handler_bootcmd.py
@@ -3,17 +3,11 @@
3from cloudinit.config import cc_bootcmd3from cloudinit.config import cc_bootcmd
4from cloudinit.sources import DataSourceNone4from cloudinit.sources import DataSourceNone
5from cloudinit import (distros, helpers, cloud, util)5from cloudinit import (distros, helpers, cloud, util)
6from cloudinit.tests.helpers import CiTestCase, mock, skipIf6from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema
77
8import logging8import logging
9import tempfile9import tempfile
1010
11try:
12 import jsonschema
13 assert jsonschema # avoid pyflakes error F401: import unused
14 _missing_jsonschema_dep = False
15except ImportError:
16 _missing_jsonschema_dep = True
1711
18LOG = logging.getLogger(__name__)12LOG = logging.getLogger(__name__)
1913
@@ -69,10 +63,10 @@ class TestBootcmd(CiTestCase):
69 cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, [])63 cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, [])
70 self.assertIn('Failed to shellify bootcmd', self.logs.getvalue())64 self.assertIn('Failed to shellify bootcmd', self.logs.getvalue())
71 self.assertEqual(65 self.assertEqual(
72 "'int' object is not iterable",66 "Input to shellify was type 'int'. Expected list or tuple.",
73 str(context_manager.exception))67 str(context_manager.exception))
7468
75 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")69 @skipUnlessJsonSchema()
76 def test_handler_schema_validation_warns_non_array_type(self):70 def test_handler_schema_validation_warns_non_array_type(self):
77 """Schema validation warns of non-array type for bootcmd key.71 """Schema validation warns of non-array type for bootcmd key.
7872
@@ -88,7 +82,7 @@ class TestBootcmd(CiTestCase):
88 self.logs.getvalue())82 self.logs.getvalue())
89 self.assertIn('Failed to shellify', self.logs.getvalue())83 self.assertIn('Failed to shellify', self.logs.getvalue())
9084
91 @skipIf(_missing_jsonschema_dep, 'No python-jsonschema dependency')85 @skipUnlessJsonSchema()
92 def test_handler_schema_validation_warns_non_array_item_type(self):86 def test_handler_schema_validation_warns_non_array_item_type(self):
93 """Schema validation warns of non-array or string bootcmd items.87 """Schema validation warns of non-array or string bootcmd items.
9488
@@ -98,7 +92,7 @@ class TestBootcmd(CiTestCase):
98 invalid_config = {92 invalid_config = {
99 'bootcmd': ['ls /', 20, ['wget', 'http://stuff/blah'], {'a': 'n'}]}93 'bootcmd': ['ls /', 20, ['wget', 'http://stuff/blah'], {'a': 'n'}]}
100 cc = self._get_cloud('ubuntu')94 cc = self._get_cloud('ubuntu')
101 with self.assertRaises(RuntimeError) as context_manager:95 with self.assertRaises(TypeError) as context_manager:
102 cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, [])96 cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, [])
103 expected_warnings = [97 expected_warnings = [
104 'bootcmd.1: 20 is not valid under any of the given schemas',98 'bootcmd.1: 20 is not valid under any of the given schemas',
@@ -110,7 +104,8 @@ class TestBootcmd(CiTestCase):
110 self.assertIn(warning, logs)104 self.assertIn(warning, logs)
111 self.assertIn('Failed to shellify', logs)105 self.assertIn('Failed to shellify', logs)
112 self.assertEqual(106 self.assertEqual(
113 'Unable to shellify type int which is not a list or string',107 ("Unable to shellify type 'int'. Expected list, string, tuple. "
108 "Got: 20"),
114 str(context_manager.exception))109 str(context_manager.exception))
115110
116 def test_handler_creates_and_runs_bootcmd_script_with_instance_id(self):111 def test_handler_creates_and_runs_bootcmd_script_with_instance_id(self):
diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
index 28a8455..695897c 100644
--- a/tests/unittests/test_handler/test_handler_ntp.py
+++ b/tests/unittests/test_handler/test_handler_ntp.py
@@ -3,7 +3,8 @@
3from cloudinit.config import cc_ntp3from cloudinit.config import cc_ntp
4from cloudinit.sources import DataSourceNone4from cloudinit.sources import DataSourceNone
5from cloudinit import (distros, helpers, cloud, util)5from cloudinit import (distros, helpers, cloud, util)
6from cloudinit.tests.helpers import FilesystemMockingTestCase, mock, skipIf6from cloudinit.tests.helpers import (
7 FilesystemMockingTestCase, mock, skipUnlessJsonSchema)
78
89
9import os10import os
@@ -24,13 +25,6 @@ NTP={% for host in servers|list + pools|list %}{{ host }} {% endfor -%}
24{% endif -%}25{% endif -%}
25"""26"""
2627
27try:
28 import jsonschema
29 assert jsonschema # avoid pyflakes error F401: import unused
30 _missing_jsonschema_dep = False
31except ImportError:
32 _missing_jsonschema_dep = True
33
3428
35class TestNtp(FilesystemMockingTestCase):29class TestNtp(FilesystemMockingTestCase):
3630
@@ -312,7 +306,7 @@ class TestNtp(FilesystemMockingTestCase):
312 content)306 content)
313 self.assertNotIn('Invalid config:', self.logs.getvalue())307 self.assertNotIn('Invalid config:', self.logs.getvalue())
314308
315 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")309 @skipUnlessJsonSchema()
316 def test_ntp_handler_schema_validation_warns_non_string_item_type(self):310 def test_ntp_handler_schema_validation_warns_non_string_item_type(self):
317 """Ntp schema validation warns of non-strings in pools or servers.311 """Ntp schema validation warns of non-strings in pools or servers.
318312
@@ -333,7 +327,7 @@ class TestNtp(FilesystemMockingTestCase):
333 content = stream.read()327 content = stream.read()
334 self.assertEqual("servers ['valid', None]\npools [123]\n", content)328 self.assertEqual("servers ['valid', None]\npools [123]\n", content)
335329
336 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")330 @skipUnlessJsonSchema()
337 def test_ntp_handler_schema_validation_warns_of_non_array_type(self):331 def test_ntp_handler_schema_validation_warns_of_non_array_type(self):
338 """Ntp schema validation warns of non-array pools or servers types.332 """Ntp schema validation warns of non-array pools or servers types.
339333
@@ -354,7 +348,7 @@ class TestNtp(FilesystemMockingTestCase):
354 content = stream.read()348 content = stream.read()
355 self.assertEqual("servers non-array\npools 123\n", content)349 self.assertEqual("servers non-array\npools 123\n", content)
356350
357 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")351 @skipUnlessJsonSchema()
358 def test_ntp_handler_schema_validation_warns_invalid_key_present(self):352 def test_ntp_handler_schema_validation_warns_invalid_key_present(self):
359 """Ntp schema validation warns of invalid keys present in ntp config.353 """Ntp schema validation warns of invalid keys present in ntp config.
360354
@@ -378,7 +372,7 @@ class TestNtp(FilesystemMockingTestCase):
378 "servers []\npools ['0.mycompany.pool.ntp.org']\n",372 "servers []\npools ['0.mycompany.pool.ntp.org']\n",
379 content)373 content)
380374
381 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")375 @skipUnlessJsonSchema()
382 def test_ntp_handler_schema_validation_warns_of_duplicates(self):376 def test_ntp_handler_schema_validation_warns_of_duplicates(self):
383 """Ntp schema validation warns of duplicates in servers or pools.377 """Ntp schema validation warns of duplicates in servers or pools.
384378
diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
index 5aa3c49..c2a7f9f 100644
--- a/tests/unittests/test_handler/test_handler_resizefs.py
+++ b/tests/unittests/test_handler/test_handler_resizefs.py
@@ -7,21 +7,13 @@ from collections import namedtuple
7import logging7import logging
8import textwrap8import textwrap
99
10from cloudinit.tests.helpers import (CiTestCase, mock, skipIf, util,10from cloudinit.tests.helpers import (
11 wrap_and_call)11 CiTestCase, mock, skipUnlessJsonSchema, util, wrap_and_call)
1212
1313
14LOG = logging.getLogger(__name__)14LOG = logging.getLogger(__name__)
1515
1616
17try:
18 import jsonschema
19 assert jsonschema # avoid pyflakes error F401: import unused
20 _missing_jsonschema_dep = False
21except ImportError:
22 _missing_jsonschema_dep = True
23
24
25class TestResizefs(CiTestCase):17class TestResizefs(CiTestCase):
26 with_logs = True18 with_logs = True
2719
@@ -76,7 +68,7 @@ class TestResizefs(CiTestCase):
76 'DEBUG: Skipping module named cc_resizefs, resizing disabled\n',68 'DEBUG: Skipping module named cc_resizefs, resizing disabled\n',
77 self.logs.getvalue())69 self.logs.getvalue())
7870
79 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")71 @skipUnlessJsonSchema()
80 def test_handle_schema_validation_logs_invalid_resize_rootfs_value(self):72 def test_handle_schema_validation_logs_invalid_resize_rootfs_value(self):
81 """The handle reports json schema violations as a warning.73 """The handle reports json schema violations as a warning.
8274
diff --git a/tests/unittests/test_handler/test_handler_runcmd.py b/tests/unittests/test_handler/test_handler_runcmd.py
index 374c1d3..dbbb271 100644
--- a/tests/unittests/test_handler/test_handler_runcmd.py
+++ b/tests/unittests/test_handler/test_handler_runcmd.py
@@ -3,19 +3,13 @@
3from cloudinit.config import cc_runcmd3from cloudinit.config import cc_runcmd
4from cloudinit.sources import DataSourceNone4from cloudinit.sources import DataSourceNone
5from cloudinit import (distros, helpers, cloud, util)5from cloudinit import (distros, helpers, cloud, util)
6from cloudinit.tests.helpers import FilesystemMockingTestCase, skipIf6from cloudinit.tests.helpers import (
7 FilesystemMockingTestCase, skipUnlessJsonSchema)
78
8import logging9import logging
9import os10import os
10import stat11import stat
1112
12try:
13 import jsonschema
14 assert jsonschema # avoid pyflakes error F401: import unused
15 _missing_jsonschema_dep = False
16except ImportError:
17 _missing_jsonschema_dep = True
18
19LOG = logging.getLogger(__name__)13LOG = logging.getLogger(__name__)
2014
2115
@@ -56,7 +50,7 @@ class TestRuncmd(FilesystemMockingTestCase):
56 ' /var/lib/cloud/instances/iid-datasource-none/scripts/runcmd',50 ' /var/lib/cloud/instances/iid-datasource-none/scripts/runcmd',
57 self.logs.getvalue())51 self.logs.getvalue())
5852
59 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")53 @skipUnlessJsonSchema()
60 def test_handler_schema_validation_warns_non_array_type(self):54 def test_handler_schema_validation_warns_non_array_type(self):
61 """Schema validation warns of non-array type for runcmd key.55 """Schema validation warns of non-array type for runcmd key.
6256
@@ -71,7 +65,7 @@ class TestRuncmd(FilesystemMockingTestCase):
71 self.logs.getvalue())65 self.logs.getvalue())
72 self.assertIn('Failed to shellify', self.logs.getvalue())66 self.assertIn('Failed to shellify', self.logs.getvalue())
7367
74 @skipIf(_missing_jsonschema_dep, 'No python-jsonschema dependency')68 @skipUnlessJsonSchema()
75 def test_handler_schema_validation_warns_non_array_item_type(self):69 def test_handler_schema_validation_warns_non_array_item_type(self):
76 """Schema validation warns of non-array or string runcmd items.70 """Schema validation warns of non-array or string runcmd items.
7771
diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/test_handler/test_handler_set_hostname.py
index abdc17e..d09ec23 100644
--- a/tests/unittests/test_handler/test_handler_set_hostname.py
+++ b/tests/unittests/test_handler/test_handler_set_hostname.py
@@ -11,6 +11,7 @@ from cloudinit.tests import helpers as t_help
1111
12from configobj import ConfigObj12from configobj import ConfigObj
13import logging13import logging
14import os
14import shutil15import shutil
15from six import BytesIO16from six import BytesIO
16import tempfile17import tempfile
@@ -19,14 +20,18 @@ LOG = logging.getLogger(__name__)
1920
2021
21class TestHostname(t_help.FilesystemMockingTestCase):22class TestHostname(t_help.FilesystemMockingTestCase):
23
24 with_logs = True
25
22 def setUp(self):26 def setUp(self):
23 super(TestHostname, self).setUp()27 super(TestHostname, self).setUp()
24 self.tmp = tempfile.mkdtemp()28 self.tmp = tempfile.mkdtemp()
29 util.ensure_dir(os.path.join(self.tmp, 'data'))
25 self.addCleanup(shutil.rmtree, self.tmp)30 self.addCleanup(shutil.rmtree, self.tmp)
2631
27 def _fetch_distro(self, kind):32 def _fetch_distro(self, kind):
28 cls = distros.fetch(kind)33 cls = distros.fetch(kind)
29 paths = helpers.Paths({})34 paths = helpers.Paths({'cloud_dir': self.tmp})
30 return cls(kind, {}, paths)35 return cls(kind, {}, paths)
3136
32 def test_write_hostname_rhel(self):37 def test_write_hostname_rhel(self):
@@ -34,7 +39,7 @@ class TestHostname(t_help.FilesystemMockingTestCase):
34 'hostname': 'blah.blah.blah.yahoo.com',39 'hostname': 'blah.blah.blah.yahoo.com',
35 }40 }
36 distro = self._fetch_distro('rhel')41 distro = self._fetch_distro('rhel')
37 paths = helpers.Paths({})42 paths = helpers.Paths({'cloud_dir': self.tmp})
38 ds = None43 ds = None
39 cc = cloud.Cloud(ds, paths, {}, distro, None)44 cc = cloud.Cloud(ds, paths, {}, distro, None)
40 self.patchUtils(self.tmp)45 self.patchUtils(self.tmp)
@@ -51,7 +56,7 @@ class TestHostname(t_help.FilesystemMockingTestCase):
51 'hostname': 'blah.blah.blah.yahoo.com',56 'hostname': 'blah.blah.blah.yahoo.com',
52 }57 }
53 distro = self._fetch_distro('debian')58 distro = self._fetch_distro('debian')
54 paths = helpers.Paths({})59 paths = helpers.Paths({'cloud_dir': self.tmp})
55 ds = None60 ds = None
56 cc = cloud.Cloud(ds, paths, {}, distro, None)61 cc = cloud.Cloud(ds, paths, {}, distro, None)
57 self.patchUtils(self.tmp)62 self.patchUtils(self.tmp)
@@ -65,7 +70,7 @@ class TestHostname(t_help.FilesystemMockingTestCase):
65 'hostname': 'blah.blah.blah.suse.com',70 'hostname': 'blah.blah.blah.suse.com',
66 }71 }
67 distro = self._fetch_distro('sles')72 distro = self._fetch_distro('sles')
68 paths = helpers.Paths({})73 paths = helpers.Paths({'cloud_dir': self.tmp})
69 ds = None74 ds = None
70 cc = cloud.Cloud(ds, paths, {}, distro, None)75 cc = cloud.Cloud(ds, paths, {}, distro, None)
71 self.patchUtils(self.tmp)76 self.patchUtils(self.tmp)
@@ -74,4 +79,48 @@ class TestHostname(t_help.FilesystemMockingTestCase):
74 contents = util.load_file(distro.hostname_conf_fn)79 contents = util.load_file(distro.hostname_conf_fn)
75 self.assertEqual('blah', contents.strip())80 self.assertEqual('blah', contents.strip())
7681
82 def test_multiple_calls_skips_unchanged_hostname(self):
83 """Only new hostname or fqdn values will generate a hostname call."""
84 distro = self._fetch_distro('debian')
85 paths = helpers.Paths({'cloud_dir': self.tmp})
86 ds = None
87 cc = cloud.Cloud(ds, paths, {}, distro, None)
88 self.patchUtils(self.tmp)
89 cc_set_hostname.handle(
90 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, [])
91 contents = util.load_file("/etc/hostname")
92 self.assertEqual('hostname1', contents.strip())
93 cc_set_hostname.handle(
94 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, [])
95 self.assertIn(
96 'DEBUG: No hostname changes. Skipping set-hostname\n',
97 self.logs.getvalue())
98 cc_set_hostname.handle(
99 'cc_set_hostname', {'hostname': 'hostname2.me.com'}, cc, LOG, [])
100 contents = util.load_file("/etc/hostname")
101 self.assertEqual('hostname2', contents.strip())
102 self.assertIn(
103 'Non-persistently setting the system hostname to hostname2',
104 self.logs.getvalue())
105
106 def test_error_on_distro_set_hostname_errors(self):
107 """Raise SetHostnameError on exceptions from distro.set_hostname."""
108 distro = self._fetch_distro('debian')
109
110 def set_hostname_error(hostname, fqdn):
111 raise Exception("OOPS on: %s" % fqdn)
112
113 distro.set_hostname = set_hostname_error
114 paths = helpers.Paths({'cloud_dir': self.tmp})
115 ds = None
116 cc = cloud.Cloud(ds, paths, {}, distro, None)
117 self.patchUtils(self.tmp)
118 with self.assertRaises(cc_set_hostname.SetHostnameError) as ctx_mgr:
119 cc_set_hostname.handle(
120 'somename', {'hostname': 'hostname1.me.com'}, cc, LOG, [])
121 self.assertEqual(
122 'Failed to set the hostname to hostname1.me.com (hostname1):'
123 ' OOPS on: hostname1.me.com',
124 str(ctx_mgr.exception))
125
77# vi: ts=4 expandtab126# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
index df67a0e..1ecb6c6 100644
--- a/tests/unittests/test_handler/test_schema.py
+++ b/tests/unittests/test_handler/test_schema.py
@@ -6,7 +6,7 @@ from cloudinit.config.schema import (
6 validate_cloudconfig_schema, main)6 validate_cloudconfig_schema, main)
7from cloudinit.util import subp, write_file7from cloudinit.util import subp, write_file
88
9from cloudinit.tests.helpers import CiTestCase, mock, skipIf9from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema
1010
11from copy import copy11from copy import copy
12import os12import os
@@ -14,13 +14,6 @@ from six import StringIO
14from textwrap import dedent14from textwrap import dedent
15from yaml import safe_load15from yaml import safe_load
1616
17try:
18 import jsonschema
19 assert jsonschema # avoid pyflakes error F401: import unused
20 _missing_jsonschema_dep = False
21except ImportError:
22 _missing_jsonschema_dep = True
23
2417
25class GetSchemaTest(CiTestCase):18class GetSchemaTest(CiTestCase):
2619
@@ -73,7 +66,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase):
7366
74 with_logs = True67 with_logs = True
7568
76 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")69 @skipUnlessJsonSchema()
77 def test_validateconfig_schema_non_strict_emits_warnings(self):70 def test_validateconfig_schema_non_strict_emits_warnings(self):
78 """When strict is False validate_cloudconfig_schema emits warnings."""71 """When strict is False validate_cloudconfig_schema emits warnings."""
79 schema = {'properties': {'p1': {'type': 'string'}}}72 schema = {'properties': {'p1': {'type': 'string'}}}
@@ -82,7 +75,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase):
82 "Invalid config:\np1: -1 is not of type 'string'\n",75 "Invalid config:\np1: -1 is not of type 'string'\n",
83 self.logs.getvalue())76 self.logs.getvalue())
8477
85 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")78 @skipUnlessJsonSchema()
86 def test_validateconfig_schema_emits_warning_on_missing_jsonschema(self):79 def test_validateconfig_schema_emits_warning_on_missing_jsonschema(self):
87 """Warning from validate_cloudconfig_schema when missing jsonschema."""80 """Warning from validate_cloudconfig_schema when missing jsonschema."""
88 schema = {'properties': {'p1': {'type': 'string'}}}81 schema = {'properties': {'p1': {'type': 'string'}}}
@@ -92,7 +85,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase):
92 'Ignoring schema validation. python-jsonschema is not present',85 'Ignoring schema validation. python-jsonschema is not present',
93 self.logs.getvalue())86 self.logs.getvalue())
9487
95 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")88 @skipUnlessJsonSchema()
96 def test_validateconfig_schema_strict_raises_errors(self):89 def test_validateconfig_schema_strict_raises_errors(self):
97 """When strict is True validate_cloudconfig_schema raises errors."""90 """When strict is True validate_cloudconfig_schema raises errors."""
98 schema = {'properties': {'p1': {'type': 'string'}}}91 schema = {'properties': {'p1': {'type': 'string'}}}
@@ -102,7 +95,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase):
102 "Cloud config schema errors: p1: -1 is not of type 'string'",95 "Cloud config schema errors: p1: -1 is not of type 'string'",
103 str(context_mgr.exception))96 str(context_mgr.exception))
10497
105 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")98 @skipUnlessJsonSchema()
106 def test_validateconfig_schema_honors_formats(self):99 def test_validateconfig_schema_honors_formats(self):
107 """With strict True, validate_cloudconfig_schema errors on format."""100 """With strict True, validate_cloudconfig_schema errors on format."""
108 schema = {101 schema = {
@@ -153,7 +146,7 @@ class ValidateCloudConfigFileTest(CiTestCase):
153 self.config_file),146 self.config_file),
154 str(context_mgr.exception))147 str(context_mgr.exception))
155148
156 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")149 @skipUnlessJsonSchema()
157 def test_validateconfig_file_sctricty_validates_schema(self):150 def test_validateconfig_file_sctricty_validates_schema(self):
158 """validate_cloudconfig_file raises errors on invalid schema."""151 """validate_cloudconfig_file raises errors on invalid schema."""
159 schema = {152 schema = {
@@ -376,7 +369,7 @@ class CloudTestsIntegrationTest(CiTestCase):
376 raises Warnings or errors on invalid cloud-config schema.369 raises Warnings or errors on invalid cloud-config schema.
377 """370 """
378371
379 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")372 @skipUnlessJsonSchema()
380 def test_all_integration_test_cloud_config_schema(self):373 def test_all_integration_test_cloud_config_schema(self):
381 """Validate schema of cloud_tests yaml files looking for warnings."""374 """Validate schema of cloud_tests yaml files looking for warnings."""
382 schema = get_schema()375 schema = get_schema()
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 89ae40f..499e7c9 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -632,6 +632,24 @@ class TestSubp(helpers.CiTestCase):
632 # but by using bash, we remove dependency on another program.632 # but by using bash, we remove dependency on another program.
633 return([BASH, '-c', 'printf "$@"', 'printf'] + list(args))633 return([BASH, '-c', 'printf "$@"', 'printf'] + list(args))
634634
635 def test_subp_handles_bytestrings(self):
636 """subp can run a bytestring command if shell is True."""
637 tmp_file = self.tmp_path('test.out')
638 cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file)
639 (out, _err) = util.subp(cmd.encode('utf-8'), shell=True)
640 self.assertEqual(u'', out)
641 self.assertEqual(u'', _err)
642 self.assertEqual('HI MOM\n', util.load_file(tmp_file))
643
644 def test_subp_handles_strings(self):
645 """subp can run a string command if shell is True."""
646 tmp_file = self.tmp_path('test.out')
647 cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file)
648 (out, _err) = util.subp(cmd, shell=True)
649 self.assertEqual(u'', out)
650 self.assertEqual(u'', _err)
651 self.assertEqual('HI MOM\n', util.load_file(tmp_file))
652
635 def test_subp_handles_utf8(self):653 def test_subp_handles_utf8(self):
636 # The given bytes contain utf-8 accented characters as seen in e.g.654 # The given bytes contain utf-8 accented characters as seen in e.g.
637 # the "deja dup" package in Ubuntu.655 # the "deja dup" package in Ubuntu.

Subscribers

People subscribed via source and target branches