diff --git a/cloudbaseinit/plugins/common/execcmd.py b/cloudbaseinit/plugins/common/execcmd.py index 4dd1c42b..4046509b 100644 --- a/cloudbaseinit/plugins/common/execcmd.py +++ b/cloudbaseinit/plugins/common/execcmd.py @@ -42,6 +42,8 @@ TAG_REGEX = { ) } +NO_REBOOT = 0 + # important return values range RET_START = 1001 RET_END = 1003 diff --git a/cloudbaseinit/plugins/common/sethostname.py b/cloudbaseinit/plugins/common/sethostname.py index 66012bb3..9ee3a3e2 100644 --- a/cloudbaseinit/plugins/common/sethostname.py +++ b/cloudbaseinit/plugins/common/sethostname.py @@ -12,57 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. -import platform -import re - -from oslo_config import cfg from oslo_log import log as oslo_logging from cloudbaseinit.osutils import factory as osutils_factory from cloudbaseinit.plugins.common import base - -opts = [ - cfg.BoolOpt('netbios_host_name_compatibility', default=True, - help='Truncates the hostname to 15 characters for Netbios ' - 'compatibility'), -] - -CONF = cfg.CONF -CONF.register_opts(opts) +from cloudbaseinit.utils import hostname LOG = oslo_logging.getLogger(__name__) -NETBIOS_HOST_NAME_MAX_LEN = 15 - class SetHostNamePlugin(base.BasePlugin): def execute(self, service, shared_data): osutils = osutils_factory.get_os_utils() - metadata_host_name = service.get_host_name() + if not metadata_host_name: LOG.debug('Hostname not found in metadata') return base.PLUGIN_EXECUTION_DONE, False - metadata_host_name = metadata_host_name.split('.', 1)[0] - - if (len(metadata_host_name) > NETBIOS_HOST_NAME_MAX_LEN and - CONF.netbios_host_name_compatibility): - new_host_name = metadata_host_name[:NETBIOS_HOST_NAME_MAX_LEN] - LOG.warn('Truncating host name for Netbios compatibility. ' - 'Old name: %(metadata_host_name)s, new name: ' - '%(new_host_name)s' % - {'metadata_host_name': metadata_host_name, - 'new_host_name': new_host_name}) - else: - new_host_name = metadata_host_name - - new_host_name = re.sub(r'-$', '0', new_host_name) - if platform.node().lower() == new_host_name.lower(): - LOG.debug("Hostname already set to: %s" % new_host_name) - reboot_required = False - else: - LOG.info("Setting hostname: %s" % new_host_name) - reboot_required = osutils.set_host_name(new_host_name) + (_, reboot_required) = hostname.set_hostname( + osutils, metadata_host_name) return base.PLUGIN_EXECUTION_DONE, reboot_required diff --git a/cloudbaseinit/plugins/common/userdataplugins/cloudconfig.py b/cloudbaseinit/plugins/common/userdataplugins/cloudconfig.py index aa71c791..ecba04ee 100644 --- a/cloudbaseinit/plugins/common/userdataplugins/cloudconfig.py +++ b/cloudbaseinit/plugins/common/userdataplugins/cloudconfig.py @@ -17,6 +17,7 @@ from oslo_config import cfg from oslo_log import log as oslo_logging import yaml +from cloudbaseinit.plugins.common import execcmd from cloudbaseinit.plugins.common.userdataplugins import base from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import ( factory @@ -37,7 +38,6 @@ OPTS = [ CONF = cfg.CONF CONF.register_opts(OPTS) DEFAULT_ORDER_VALUE = 999 -REBOOT = 1001 class CloudConfigError(Exception): @@ -80,7 +80,7 @@ class CloudConfigPluginExecutor(object): def execute(self): """Call each plugin, in the order requested by the user.""" - reboot = 0 + reboot = execcmd.NO_REBOOT plugins = factory.load_plugins() for plugin_name, value in self._expected_plugins: method = plugins.get(plugin_name) @@ -91,7 +91,7 @@ class CloudConfigPluginExecutor(object): try: requires_reboot = method(value) if requires_reboot: - reboot = REBOOT + reboot = execcmd.RET_END except Exception: LOG.exception("Processing plugin %s failed", plugin_name) return reboot diff --git a/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/factory.py b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/factory.py index b5b3a7c7..8bbab399 100644 --- a/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/factory.py +++ b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/factory.py @@ -22,6 +22,8 @@ PLUGINS = { 'cloudconfigplugins.write_files.WriteFilesPlugin', 'set_timezone': 'cloudbaseinit.plugins.common.userdataplugins.' 'cloudconfigplugins.set_timezone.SetTimezonePlugin', + 'set_hostname': 'cloudbaseinit.plugins.common.userdataplugins.' + 'cloudconfigplugins.set_hostname.SetHostnamePlugin', } diff --git a/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/set_hostname.py b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/set_hostname.py new file mode 100644 index 00000000..6b28361e --- /dev/null +++ b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/set_hostname.py @@ -0,0 +1,38 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as oslo_logging + +from cloudbaseinit.osutils import factory +from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import ( + base +) +from cloudbaseinit.utils import hostname + + +LOG = oslo_logging.getLogger(__name__) + + +class SetHostnamePlugin(base.BaseCloudConfigPlugin): + """Change the hostname for the underlying platform. + + If the timezone is changed a restart will be required. + + """ + + def process(self, data): + LOG.info("Changing hostname to %r", data) + osutils = factory.get_os_utils() + _, reboot_required = hostname.set_hostname(osutils, data) + return reboot_required diff --git a/cloudbaseinit/tests/plugins/common/test_sethostname.py b/cloudbaseinit/tests/plugins/common/test_sethostname.py index b22b535e..5fad7cd2 100644 --- a/cloudbaseinit/tests/plugins/common/test_sethostname.py +++ b/cloudbaseinit/tests/plugins/common/test_sethostname.py @@ -21,7 +21,6 @@ except ImportError: from cloudbaseinit.plugins.common import base from cloudbaseinit.plugins.common import sethostname -from cloudbaseinit.tests.metadata import fake_json_response from cloudbaseinit.tests import testutils @@ -29,74 +28,37 @@ class SetHostNamePluginPluginTests(unittest.TestCase): def setUp(self): self._sethostname_plugin = sethostname.SetHostNamePlugin() - self.fake_data = fake_json_response.get_fake_metadata_json( - '2013-04-04') @testutils.ConfPatcher('netbios_host_name_compatibility', True) - @mock.patch('platform.node') @mock.patch('cloudbaseinit.osutils.factory.get_os_utils') - def _test_execute(self, mock_get_os_utils, mock_node, hostname_exists=True, - hostname_already_set=False, new_hostname_length=1, - hostname_truncate_to_zero=False): + @mock.patch('cloudbaseinit.utils.hostname.set_hostname') + def _test_execute(self, mock_set_hostname, mock_get_os_utils, + hostname_exists=False): + new_hostname = 'hostname' + shared_data = 'fake_shared_data' + mock_get_os_utils.return_value = None mock_service = mock.MagicMock() - mock_osutils = mock.MagicMock() - fake_shared_data = 'fake data' - new_hostname = 'x' * new_hostname_length - - if hostname_truncate_to_zero: - new_hostname = ('%s-') % new_hostname[:-1] - if hostname_exists: mock_service.get_host_name.return_value = new_hostname else: mock_service.get_host_name.return_value = None + mock_set_hostname.return_value = (new_hostname, True) - mock_get_os_utils.return_value = mock_osutils - mock_get_os_utils.return_value.set_host_name.return_value = True - - if hostname_exists is True: - length = sethostname.NETBIOS_HOST_NAME_MAX_LEN - hostname = new_hostname.split('.', 1)[0] - if len(new_hostname) > length: - hostname = hostname[:length] - if hostname_truncate_to_zero: - hostname = ('%s0') % hostname[:-1] - if hostname_already_set: - mock_node.return_value = hostname - else: - mock_node.return_value = 'fake_old_value' - - response = self._sethostname_plugin.execute(mock_service, - fake_shared_data) + response = self._sethostname_plugin.execute(mock_service, shared_data) mock_service.get_host_name.assert_called_once_with() - if hostname_exists is True: - mock_get_os_utils.assert_called_once_with() - if hostname_already_set: - self.assertFalse(mock_osutils.set_host_name.called) - else: - mock_osutils.set_host_name.assert_called_once_with(hostname) + if hostname_exists: + mock_set_hostname.assert_called_once_with( + None, new_hostname) + else: + self.assertFalse(mock_set_hostname.called) - self.assertEqual((base.PLUGIN_EXECUTION_DONE, - hostname_exists and not hostname_already_set), + self.assertEqual((base.PLUGIN_EXECUTION_DONE, hostname_exists), response) - def test_execute_hostname_already_set(self): - self._test_execute(hostname_already_set=True) - - def test_execute_hostname_to_be_truncated(self): - self._test_execute( - new_hostname_length=sethostname.NETBIOS_HOST_NAME_MAX_LEN + 1) - - def test_execute_no_truncate_needed(self): - self._test_execute( - new_hostname_length=sethostname.NETBIOS_HOST_NAME_MAX_LEN) - - def test_execute_truncate_to_zero(self): - self._test_execute( - new_hostname_length=sethostname.NETBIOS_HOST_NAME_MAX_LEN, - hostname_truncate_to_zero=True) + def test_execute_new_hostname(self): + self._test_execute(hostname_exists=True) def test_execute_no_hostname(self): self._test_execute(hostname_exists=False) diff --git a/cloudbaseinit/tests/utils/test_hostname.py b/cloudbaseinit/tests/utils/test_hostname.py new file mode 100644 index 00000000..ae53962f --- /dev/null +++ b/cloudbaseinit/tests/utils/test_hostname.py @@ -0,0 +1,77 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +from oslo_config import cfg + +from cloudbaseinit.tests import testutils +from cloudbaseinit.utils import hostname + +CONF = cfg.CONF + + +class HostnameUtilsTest(unittest.TestCase): + + @testutils.ConfPatcher('netbios_host_name_compatibility', True) + @mock.patch('platform.node') + def _test_set_hostname(self, mock_node, new_hostname='hostname', + expected_new_hostname='hostname', + hostname_already_set=False): + mock_osutils = mock.MagicMock() + mock_osutils.set_host_name.return_value = True + + if hostname_already_set: + mock_node.return_value = expected_new_hostname + else: + mock_node.return_value = 'fake_old_hostname' + + (new_hostname, reboot_required) = hostname.set_hostname( + mock_osutils, new_hostname) + + if hostname_already_set: + self.assertFalse(mock_osutils.set_host_name.called) + else: + mock_osutils.set_host_name.assert_called_once_with( + expected_new_hostname) + + self.assertEqual((new_hostname, reboot_required), + (expected_new_hostname, not hostname_already_set)) + + def test_execute_hostname_already_set(self): + self._test_set_hostname(hostname_already_set=True) + + def test_execute_hostname_to_be_truncated(self): + new_hostname = 'x' * (hostname.NETBIOS_HOST_NAME_MAX_LEN + 1) + expected_new_hostname = new_hostname[:-1] + self._test_set_hostname(new_hostname=new_hostname, + expected_new_hostname=expected_new_hostname) + + def test_execute_no_truncate_needed(self): + new_hostname = 'x' * hostname.NETBIOS_HOST_NAME_MAX_LEN + expected_new_hostname = 'x' * hostname.NETBIOS_HOST_NAME_MAX_LEN + self._test_set_hostname(new_hostname=new_hostname, + expected_new_hostname=expected_new_hostname) + + def test_execute_truncate_to_zero(self): + new_hostname = 'x' * (hostname.NETBIOS_HOST_NAME_MAX_LEN - 1) + '-' + expected_new_hostname = 'x' * ( + hostname.NETBIOS_HOST_NAME_MAX_LEN - 1) + '0' + self._test_set_hostname(new_hostname=new_hostname, + expected_new_hostname=expected_new_hostname) diff --git a/cloudbaseinit/utils/hostname.py b/cloudbaseinit/utils/hostname.py new file mode 100644 index 00000000..a942f81c --- /dev/null +++ b/cloudbaseinit/utils/hostname.py @@ -0,0 +1,69 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import platform +import re + +from oslo_config import cfg +from oslo_log import log as oslo_logging + +opts = [ + cfg.BoolOpt('netbios_host_name_compatibility', default=True, + help='Truncates the hostname to 15 characters for Netbios ' + 'compatibility'), +] + +CONF = cfg.CONF +CONF.register_opts(opts) + +LOG = oslo_logging.getLogger(__name__) + +NETBIOS_HOST_NAME_MAX_LEN = 15 + + +def set_hostname(osutils, hostname): + """Change the hostname for the underlying platform. + + If netbios_host_name_compatibility is set to True in the configuration + file, then the hostname is truncated to NETBIOS_HOST_NAME_MAX_LEN. + + Params: + osutils: instance of osutils + hostname: the desired hostname + + Returns: + (new_hostname, reboot_required) + new_hostname: the possibly truncated hostname + reboot_required: True if the hostname was changed and a reboot is + required, False otherwise. + + """ + hostname = hostname.split('.', 1)[0] + if (len(hostname) > NETBIOS_HOST_NAME_MAX_LEN and + CONF.netbios_host_name_compatibility): + LOG.warn('Truncating host name for Netbios compatibility. ' + 'Old name: %(old_hostname)s, new name: ' + '%(new_hostname)s' % + {'old_hostname': hostname, + 'new_hostname': hostname[:NETBIOS_HOST_NAME_MAX_LEN]}) + hostname = hostname[:NETBIOS_HOST_NAME_MAX_LEN] + hostname = re.sub(r'-$', '0', hostname) + if platform.node().lower() == hostname.lower(): + LOG.debug("Hostname already set to: %s" % hostname) + reboot_required = False + else: + LOG.info("Setting hostname: %s" % hostname) + reboot_required = osutils.set_host_name(hostname) + + return hostname, reboot_required