diff options
28 files changed, 277 insertions, 112 deletions
diff --git a/devstack/lib/ironic b/devstack/lib/ironic index 95aba3c6a..78b45f812 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -2028,12 +2028,10 @@ function create_ovs_taps { port_id=$(openstack --os-cloud devstack-admin port create --network ${ironic_net_id} temp_port -c id -f value) die_if_not_set $LINENO port_id "Failed to create neutron port" - # intentional sleep to make sure the tag has been set to port - sleep 10 - local tapdev - tapdev=$(sudo ip netns exec qdhcp-${ironic_net_id} ip link list | grep " tap" | cut -d':' -f2 | cut -d'@' -f1 | cut -b2-) - die_if_not_set $LINENO tapdev "Failed to get tap device id" + local tapdev_cmd="sudo ip netns exec qdhcp-${ironic_net_id} ip link list | grep ' tap' | cut -d':' -f2 | cut -d'@' -f1 | cut -b2- | grep '^tap'" + # retry tap device discovery to make sure the tag has been set to port + tapdev=$(test_with_retry "$tapdev_cmd" "Failed to get tap device id" 20 1) local tag_id tag_id=$(sudo ovs-vsctl get port ${tapdev} tag) die_if_not_set $LINENO tag_id "Failed to get tag id" @@ -2747,20 +2745,6 @@ function configure_tftpd { echo "re ^(^/) $IRONIC_TFTPBOOT_DIR/\1" >>$IRONIC_TFTPBOOT_DIR/map-file echo "re ^([^/]) $IRONIC_TFTPBOOT_DIR/\1" >>$IRONIC_TFTPBOOT_DIR/map-file - # Write a grub.cfg redirect for the ubuntu grub. The fedora grub - # will fetch the generated grub.cfg-01-<mac> directly - grub_dir=$IRONIC_TFTPBOOT_DIR/grub - mkdir -p $grub_dir - cat << EOF > $grub_dir/grub.cfg -set default=master -set timeout=1 -set hidden_timeout_quiet=false - -menuentry "master" { -configfile $IRONIC_TFTPBOOT_DIR/\$net_default_mac.conf -} -EOF - chmod 644 $grub_dir/grub.cfg else echo "r ^([^/]) $IRONIC_TFTPBOOT_DIR/\1" >$IRONIC_TFTPBOOT_DIR/map-file echo "r ^(/tftpboot/) $IRONIC_TFTPBOOT_DIR/\2" >>$IRONIC_TFTPBOOT_DIR/map-file diff --git a/doc/source/install/configure-pxe.rst b/doc/source/install/configure-pxe.rst index 0d084d839..59346ce14 100644 --- a/doc/source/install/configure-pxe.rst +++ b/doc/source/install/configure-pxe.rst @@ -157,38 +157,6 @@ the PXE UEFI environment. sudo cp /usr/lib64/efi/shim.efi /tftpboot/bootx64.efi sudo cp /usr/lib/grub2/x86_64-efi/grub.efi /tftpboot/grubx64.efi -#. Create master grub.cfg: - - Ubuntu: Create grub.cfg under ``/tftpboot/grub`` directory:: - - GRUB_DIR=/tftpboot/grub - - Fedora: Create grub.cfg under ``/tftpboot/EFI/fedora`` directory:: - - GRUB_DIR=/tftpboot/EFI/fedora - - RHEL8/CentOS8: Create grub.cfg under ``/tftpboot/EFI/centos`` directory:: - - GRUB_DIR=/tftpboot/EFI/centos - - SUSE: Create grub.cfg under ``/tftpboot/boot/grub`` directory:: - - GRUB_DIR=/tftpboot/boot/grub - - Create directory ``GRUB_DIR``:: - - sudo mkdir -p $GRUB_DIR - - This file is used to redirect grub to baremetal node specific config file. - It redirects it to specific grub config file based on DHCP IP assigned to - baremetal node. - - .. literalinclude:: ../../../ironic/drivers/modules/master_grub_cfg.txt - - Change the permission of grub.cfg:: - - sudo chmod 644 $GRUB_DIR/grub.cfg - #. Update the bare metal node with ``boot_mode:uefi`` capability in node's properties field. See :ref:`boot_mode_support` for details. diff --git a/doc/source/install/standalone/configure.rst b/doc/source/install/standalone/configure.rst index 906e58b2c..fc540d986 100644 --- a/doc/source/install/standalone/configure.rst +++ b/doc/source/install/standalone/configure.rst @@ -92,6 +92,20 @@ You should make the following changes to ``/etc/ironic/ironic.conf``: username = myName password = myPassword +#. Starting with the Yoga release series, you can use a combined API+conductor + service and completely disable the RPC. Set + + .. code-block:: ini + + [DEFAULT] + rpc_transport = none + + and use the ``ironic`` executable to start the combined service. + + .. note:: + The combined service also works with RPC enabled, which can be useful for + some deployments, but may not be advisable for all security models. + Using CLI --------- diff --git a/ironic/api/wsgi.py b/ironic/api/wsgi.py index 467389701..bf98de90f 100644 --- a/ironic/api/wsgi.py +++ b/ironic/api/wsgi.py @@ -30,6 +30,7 @@ def initialize_wsgi_app(argv=sys.argv): i18n.install('ironic') service.prepare_command(argv) + service.ensure_rpc_transport() LOG.debug("Configuration:") CONF.log_opt_values(LOG, log.DEBUG) diff --git a/ironic/cmd/api.py b/ironic/cmd/api.py index 4a4b381c8..2323c4b09 100644 --- a/ironic/cmd/api.py +++ b/ironic/cmd/api.py @@ -33,6 +33,7 @@ LOG = log.getLogger(__name__) def main(): # Parse config file and command line options, then start logging ironic_service.prepare_service('ironic_api', sys.argv) + ironic_service.ensure_rpc_transport() # Build and start the WSGI app launcher = ironic_service.process_launcher() diff --git a/ironic/cmd/conductor.py b/ironic/cmd/conductor.py index 19fb05cb4..843185890 100644 --- a/ironic/cmd/conductor.py +++ b/ironic/cmd/conductor.py @@ -58,6 +58,7 @@ def main(): # Parse config file and command line options, then start logging ironic_service.prepare_service('ironic_conductor', sys.argv) + ironic_service.ensure_rpc_transport(CONF) mgr = rpc_service.RPCService(CONF.host, 'ironic.conductor.manager', diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 02c9e9552..e87f73185 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -24,6 +24,7 @@ import jinja2 from oslo_concurrency import processutils from oslo_log import log as logging from oslo_utils import excutils +from oslo_utils import fileutils from ironic.common import dhcp_factory from ironic.common import exception @@ -348,7 +349,8 @@ def create_pxe_config(task, pxe_options, template=None, ipxe_enabled=False): 'DISK_IDENTIFIER': pxe_config_disk_ident} pxe_config = utils.render_template(template, params) - utils.write_to_file(pxe_config_file_path, pxe_config) + utils.write_to_file(pxe_config_file_path, pxe_config, + CONF.pxe.file_permission) # Always write the mac addresses _link_mac_pxe_configs(task, ipxe_enabled=ipxe_enabled) @@ -381,7 +383,8 @@ def create_ipxe_boot_script(): # which should be rather rare if (not os.path.isfile(bootfile_path) or not utils.file_has_content(bootfile_path, boot_script)): - utils.write_to_file(bootfile_path, boot_script) + utils.write_to_file(bootfile_path, boot_script, + CONF.pxe.file_permission) def clean_up_pxe_config(task, ipxe_enabled=False): @@ -1168,7 +1171,8 @@ def prepare_instance_kickstart_config(task, image_info, anaconda_boot=False): ks_config_drive = ks_utils.prepare_config_drive(task) if ks_config_drive: ks_cfg = ks_cfg + ks_config_drive - utils.write_to_file(image_info['ks_cfg'][1], ks_cfg) + utils.write_to_file(image_info['ks_cfg'][1], ks_cfg, + CONF.pxe.file_permission) @image_cache.cleanup(priority=25) @@ -1210,8 +1214,12 @@ def cache_ramdisk_kernel(task, pxe_info, ipxe_enabled=False): LOG.debug("Fetching necessary kernel and ramdisk for node %s", node.uuid) - deploy_utils.fetch_images(ctx, TFTPImageCache(), list(t_pxe_info.values()), + images_info = list(t_pxe_info.values()) + deploy_utils.fetch_images(ctx, TFTPImageCache(), images_info, CONF.force_raw_images) + if CONF.pxe.file_permission: + for info in images_info: + os.chmod(info[1], CONF.pxe.file_permission) def clean_up_pxe_env(task, images_info, ipxe_enabled=False): @@ -1279,3 +1287,25 @@ def place_loaders_for_boot(base_path): 'the requested destination. %s' % e) LOG.error(msg) raise exception.IncorrectConfiguration(error=msg) + + +def place_common_config(): + """Place template generated config which is not node specific. + + Currently places the initial grub config for grub network boot. + """ + if not CONF.pxe.initial_grub_template: + return + + grub_dir_path = os.path.join(_get_root_dir(False), 'grub') + if not os.path.isdir(grub_dir_path): + fileutils.ensure_tree(grub_dir_path) + if CONF.pxe.dir_permission: + os.chmod(grub_dir_path, CONF.pxe.dir_permission) + + initial_grub = utils.render_template( + CONF.pxe.initial_grub_template, + {'tftp_root': _get_root_dir(False)}) + initial_grub_path = os.path.join(grub_dir_path, 'grub.cfg') + + utils.write_to_file(initial_grub_path, initial_grub) diff --git a/ironic/common/rpc_service.py b/ironic/common/rpc_service.py index bbf38d7f4..78379c981 100644 --- a/ironic/common/rpc_service.py +++ b/ironic/common/rpc_service.py @@ -53,19 +53,22 @@ class RPCService(service.Service): if CONF.rpc_transport == 'json-rpc': self.rpcserver = json_rpc.WSGIService( self.manager, serializer, context.RequestContext.from_dict) - else: + elif CONF.rpc_transport != 'none': target = messaging.Target(topic=self.topic, server=self.host) endpoints = [self.manager] self.rpcserver = rpc.get_server(target, endpoints, serializer) - self.rpcserver.start() + + if self.rpcserver is not None: + self.rpcserver.start() self.handle_signal() self.manager.init_host(admin_context) rpc.set_global_manager(self.manager) - LOG.info('Created RPC server for service %(service)s on host ' - '%(host)s.', - {'service': self.topic, 'host': self.host}) + LOG.info('Created RPC server with %(transport)s transport for service ' + '%(service)s on host %(host)s.', + {'service': self.topic, 'host': self.host, + 'transport': CONF.rpc_transport}) def stop(self): try: diff --git a/ironic/common/service.py b/ironic/common/service.py index db83c147a..c30df6f56 100644 --- a/ironic/common/service.py +++ b/ironic/common/service.py @@ -69,3 +69,11 @@ def prepare_service(name, argv=None, conf=CONF): def process_launcher(): return service.ProcessLauncher(CONF, restart_method='mutate') + + +def ensure_rpc_transport(conf=CONF): + # Only the combined ironic executable can use rpc_transport = none + if conf.rpc_transport == 'none': + raise RuntimeError("This service is not designed to work with " + "rpc_transport = none. Please use the combined " + "ironic executable or another RPC transport.") diff --git a/ironic/common/utils.py b/ironic/common/utils.py index e15083396..f37088c07 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -278,9 +278,11 @@ def rmtree_without_raise(path): {'path': path, 'e': e}) -def write_to_file(path, contents): +def write_to_file(path, contents, permission=None): with open(path, 'w') as f: f.write(contents) + if permission: + os.chmod(path, permission) def create_link_without_raise(source, link): diff --git a/ironic/conductor/base_manager.py b/ironic/conductor/base_manager.py index 935429a02..d53e6af1e 100644 --- a/ironic/conductor/base_manager.py +++ b/ironic/conductor/base_manager.py @@ -31,7 +31,6 @@ from ironic.common import driver_factory from ironic.common import exception from ironic.common import hash_ring from ironic.common.i18n import _ -from ironic.common import pxe_utils from ironic.common import release_mappings as versions from ironic.common import rpc from ironic.common import states @@ -88,11 +87,9 @@ class BaseConductorManager(object): def prepare_host(self): """Prepares host for initialization - Prepares the conductor for basic operation by removing any - existing transitory node power states and reservations which - were previously held by this host. Once that has been completed, - bootloader assets, if configured, are staged for network (PXE) boot - operations. + Prepares the conductor for basic operation by removing any existing + transitory node power states and reservations which were previously + held by this host. Under normal operation, this is also when the initial database connectivity is established for the conductor's normal operation. @@ -112,8 +109,6 @@ class BaseConductorManager(object): self.dbapi.clear_node_target_power_state(self.host) # clear all locks held by this conductor before registering self.dbapi.clear_node_reservations_for_conductor(self.host) - pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root) - pxe_utils.place_loaders_for_boot(CONF.deploy.http_root) def init_host(self, admin_context=None): """Initialize the conductor host. diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py index 6f4971be7..21139e60d 100644 --- a/ironic/conductor/rpcapi.py +++ b/ironic/conductor/rpcapi.py @@ -174,10 +174,12 @@ class ConductorAPI(object): self.client = json_rpc.Client(serializer=serializer, version_cap=version_cap) self.topic = '' - else: + elif CONF.rpc_transport != 'none': target = messaging.Target(topic=self.topic, version='1.0') self.client = rpc.get_client(target, version_cap=version_cap, serializer=serializer) + else: + self.client = None # NOTE(tenbrae): this is going to be buggy self.ring_manager = hash_ring.HashRingManager() @@ -203,6 +205,13 @@ class ConductorAPI(object): # conductor. return _LOCAL_CONTEXT + # A safeguard for the case someone uses rpc_transport=None with no + # built-in conductor. + if self.client is None: + raise exception.ServiceUnavailable( + _("Cannot use 'none' RPC to connect to remote conductor %s") + % host) + # Normal RPC path return self.client.prepare(topic=topic, version=version) @@ -276,13 +285,17 @@ class ConductorAPI(object): """Get RPC topic name for the current conductor.""" return self.topic + "." + CONF.host + def _can_send_version(self, version): + return (self.client.can_send_version(version) + if self.client is not None else True) + def can_send_create_port(self): """Return whether the RPCAPI supports the create_port method.""" - return self.client.can_send_version("1.41") + return self._can_send_version("1.41") def can_send_rescue(self): """Return whether the RPCAPI supports node rescue methods.""" - return self.client.can_send_version("1.43") + return self._can_send_version("1.43") def create_node(self, context, node_obj, topic=None): """Synchronously, have a conductor validate and create a node. @@ -1047,16 +1060,16 @@ class ConductorAPI(object): """ new_kws = {} version = '1.34' - if self.client.can_send_version('1.42'): + if self._can_send_version('1.42'): version = '1.42' new_kws['agent_version'] = agent_version - if self.client.can_send_version('1.49'): + if self._can_send_version('1.49'): version = '1.49' new_kws['agent_token'] = agent_token - if self.client.can_send_version('1.51'): + if self._can_send_version('1.51'): version = '1.51' new_kws['agent_verify_ca'] = agent_verify_ca - if self.client.can_send_version('1.54'): + if self._can_send_version('1.54'): version = '1.54' new_kws['agent_status'] = agent_status new_kws['agent_status_message'] = agent_status_message @@ -1082,7 +1095,7 @@ class ConductorAPI(object): :returns: The result of the action method, which may (or may not) be an instance of the implementing VersionedObject class. """ - if not self.client.can_send_version('1.31'): + if not self._can_send_version('1.31'): raise NotImplementedError(_('Incompatible conductor version - ' 'please upgrade ironic-conductor ' 'first')) @@ -1108,7 +1121,7 @@ class ConductorAPI(object): :returns: A tuple with the updates made to the object and the result of the action method """ - if not self.client.can_send_version('1.31'): + if not self._can_send_version('1.31'): raise NotImplementedError(_('Incompatible conductor version - ' 'please upgrade ironic-conductor ' 'first')) @@ -1133,7 +1146,7 @@ class ConductorAPI(object): upgrade :returns: The downgraded instance of objinst """ - if not self.client.can_send_version('1.31'): + if not self._can_send_version('1.31'): raise NotImplementedError(_('Incompatible conductor version - ' 'please upgrade ironic-conductor ' 'first')) diff --git a/ironic/conf/default.py b/ironic/conf/default.py index 1399b4f2e..3a6d3721d 100644 --- a/ironic/conf/default.py +++ b/ironic/conf/default.py @@ -362,7 +362,8 @@ service_opts = [ cfg.StrOpt('rpc_transport', default='oslo', choices=[('oslo', _('use oslo.messaging transport')), - ('json-rpc', _('use JSON RPC transport'))], + ('json-rpc', _('use JSON RPC transport')), + ('none', _('No RPC, only use local conductor'))], help=_('Which RPC transport implementation to use between ' 'conductor and API services')), cfg.BoolOpt('minimum_memory_warning_only', diff --git a/ironic/conf/pxe.py b/ironic/conf/pxe.py index 626c5ef6b..d96712824 100644 --- a/ironic/conf/pxe.py +++ b/ironic/conf/pxe.py @@ -204,6 +204,11 @@ opts = [ 'for bootloaders. Use example: ' 'ipxe.efi:/usr/share/ipxe/ipxe-snponly-x86_64.efi,' 'undionly.kpxe:/usr/share/ipxe/undionly.kpxe')), + cfg.StrOpt('initial_grub_template', + default=os.path.join( + '$pybasedir', 'drivers/modules/initial_grub_cfg.template'), + help=_('On ironic-conductor node, the path to the initial grub' + 'configuration template for grub network boot.')), ] diff --git a/ironic/drivers/modules/initial_grub_cfg.template b/ironic/drivers/modules/initial_grub_cfg.template new file mode 100644 index 000000000..3c1a2d76d --- /dev/null +++ b/ironic/drivers/modules/initial_grub_cfg.template @@ -0,0 +1,7 @@ +set default=initial +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "initial" { +configfile {{ tftp_root }}/$net_default_mac.conf +} diff --git a/ironic/drivers/modules/ipxe.py b/ironic/drivers/modules/ipxe.py index c80769396..5fe911652 100644 --- a/ironic/drivers/modules/ipxe.py +++ b/ironic/drivers/modules/ipxe.py @@ -16,6 +16,7 @@ iPXE Boot Interface """ from ironic.common import pxe_utils +from ironic.conf import CONF from ironic.drivers import base from ironic.drivers.modules import pxe_base @@ -28,3 +29,6 @@ class iPXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): def __init__(self): pxe_utils.create_ipxe_boot_script() + pxe_utils.place_loaders_for_boot(CONF.deploy.http_root) + # This is required to serve the iPXE binary via tftp + pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root) diff --git a/ironic/drivers/modules/master_grub_cfg.txt b/ironic/drivers/modules/master_grub_cfg.txt deleted file mode 100644 index 82847b81e..000000000 --- a/ironic/drivers/modules/master_grub_cfg.txt +++ /dev/null @@ -1,7 +0,0 @@ -set default=master -set timeout=5 -set hidden_timeout_quiet=false - -menuentry "master" { -configfile /tftpboot/$net_default_mac.conf -} diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index f3172b150..50d962fcf 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -20,9 +20,11 @@ from oslo_log import log as logging from ironic.common import boot_devices from ironic.common.i18n import _ +from ironic.common import pxe_utils from ironic.common import states from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils +from ironic.conf import CONF from ironic.drivers import base from ironic.drivers.modules import agent_base from ironic.drivers.modules import deploy_utils @@ -36,6 +38,11 @@ class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): capabilities = ['ramdisk_boot', 'pxe_boot'] + def __init__(self): + pxe_utils.place_common_config() + pxe_utils.place_loaders_for_boot(CONF.deploy.http_root) + pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root) + class PXEAnacondaDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin, base.DeployInterface): diff --git a/ironic/tests/base.py b/ironic/tests/base.py index 014581963..1045b3d34 100644 --- a/ironic/tests/base.py +++ b/ironic/tests/base.py @@ -152,6 +152,7 @@ class TestCase(oslo_test_base.BaseTestCase): group='neutron') self.config(enabled_hardware_types=['fake-hardware', 'manual-management']) + self.config(initial_grub_template=None, group='pxe') for iface in drivers_base.ALL_INTERFACES: default = None diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index 161b85932..ef1e5d1f3 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -476,7 +476,7 @@ class TestPXEUtils(db_base.DbTestCase): pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) self.assertTrue(mock_link_ip_addr.called) @mock.patch.object(pxe_utils, '_link_ip_address_pxe_configs', @@ -509,7 +509,7 @@ class TestPXEUtils(db_base.DbTestCase): pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) self.assertTrue(mock_link_ip_addr.called) @mock.patch.object(pxe_utils, '_link_ip_address_pxe_configs', @@ -540,7 +540,7 @@ class TestPXEUtils(db_base.DbTestCase): isdir_mock.assert_has_calls([]) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) self.assertTrue(mock_link_ip_address.called) @mock.patch.object(os.path, 'isdir', autospec=True) @@ -569,7 +569,7 @@ class TestPXEUtils(db_base.DbTestCase): isdir_mock.assert_has_calls([]) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) @mock.patch.object(os, 'makedirs', autospec=True) @mock.patch('ironic.common.pxe_utils._link_ip_address_pxe_configs', @@ -601,7 +601,7 @@ class TestPXEUtils(db_base.DbTestCase): pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) @mock.patch.object(os, 'makedirs', autospec=True) @mock.patch('ironic.common.pxe_utils._link_mac_pxe_configs', @@ -642,7 +642,7 @@ class TestPXEUtils(db_base.DbTestCase): pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) @mock.patch.object(os, 'makedirs', autospec=True) @mock.patch('ironic.common.pxe_utils._link_mac_pxe_configs', autospec=True) @@ -675,7 +675,7 @@ class TestPXEUtils(db_base.DbTestCase): pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path( self.node.uuid, ipxe_enabled=True) write_mock.assert_called_with(pxe_cfg_file_path, - render_mock.return_value) + render_mock.return_value, 0o644) @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.get_ip_addresses', autospec=True) @@ -714,7 +714,7 @@ class TestPXEUtils(db_base.DbTestCase): write_mock.assert_called_once_with( os.path.join(CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) render_mock.assert_called_once_with( CONF.pxe.ipxe_boot_script, {'ipxe_for_mac_uri': 'pxelinux.cfg/', @@ -736,7 +736,7 @@ class TestPXEUtils(db_base.DbTestCase): write_mock.assert_called_once_with( os.path.join(CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) render_mock.assert_called_once_with( CONF.pxe.ipxe_boot_script, {'ipxe_for_mac_uri': 'pxelinux.cfg/', @@ -755,7 +755,7 @@ class TestPXEUtils(db_base.DbTestCase): write_mock.assert_called_once_with( os.path.join(CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) render_mock.assert_called_once_with( CONF.pxe.ipxe_boot_script, {'ipxe_for_mac_uri': 'pxelinux.cfg/', @@ -1035,6 +1035,53 @@ class TestPXEUtils(db_base.DbTestCase): next(actual)) self.assertEqual('/tftpboot-path/' + address + '.conf', next(actual)) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'chmod', autospec=True) + def test_place_common_config(self, mock_chmod, mock_isdir, + mock_makedirs): + self.config(initial_grub_template=os.path.join( + '$pybasedir', + 'drivers/modules/initial_grub_cfg.template'), + group='pxe') + mock_isdir.return_value = False + self.config(group='pxe', dir_permission=0o777) + + def write_to_file(path, contents): + self.assertEqual('/tftpboot/grub/grub.cfg', path) + self.assertIn( + 'configfile /tftpboot/$net_default_mac.conf', + contents + ) + + with mock.patch('ironic.common.utils.write_to_file', + wraps=write_to_file): + pxe_utils.place_common_config() + + mock_isdir.assert_called_once_with('/tftpboot/grub') + mock_makedirs.assert_called_once_with('/tftpboot/grub', 511) + mock_chmod.assert_called_once_with('/tftpboot/grub', 0o777) + + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'chmod', autospec=True) + def test_place_common_config_existing_dirs(self, mock_chmod, mock_isdir, + mock_makedirs): + self.config(initial_grub_template=os.path.join( + '$pybasedir', + 'drivers/modules/initial_grub_cfg.template'), + group='pxe') + mock_isdir.return_value = True + + with mock.patch('ironic.common.utils.write_to_file', + autospec=True) as mock_write: + pxe_utils.place_common_config() + mock_write.assert_called_once() + + mock_isdir.assert_called_once_with('/tftpboot/grub') + mock_makedirs.assert_not_called() + mock_chmod.assert_not_called() + @mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) @mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None) @@ -1317,8 +1364,10 @@ class PXEInterfacesTestCase(db_base.DbTestCase): task, ipxe_enabled=False ) + @mock.patch.object(os, 'chmod', autospec=True) @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) - def test__cache_tftp_images_master_path(self, mock_fetch_image): + def test__cache_tftp_images_master_path(self, mock_fetch_image, + mock_chmod): temp_dir = tempfile.mkdtemp() self.config(tftp_root=temp_dir, group='pxe') self.config(tftp_master_path=os.path.join(temp_dir, @@ -1338,11 +1387,13 @@ class PXEInterfacesTestCase(db_base.DbTestCase): image_path)], True) + @mock.patch.object(os, 'chmod', autospec=True) @mock.patch.object(pxe_utils, 'TFTPImageCache', lambda: None) @mock.patch.object(pxe_utils, 'ensure_tree', autospec=True) @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) - def test_cache_ramdisk_kernel(self, mock_fetch_image, mock_ensure_tree): - fake_pxe_info = {'foo': 'bar'} + def test_cache_ramdisk_kernel(self, mock_fetch_image, mock_ensure_tree, + mock_chmod): + fake_pxe_info = pxe_utils.get_image_info(self.node) expected_path = os.path.join(CONF.pxe.tftp_root, self.node.uuid) with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: @@ -1351,12 +1402,13 @@ class PXEInterfacesTestCase(db_base.DbTestCase): mock_fetch_image.assert_called_once_with( self.context, mock.ANY, list(fake_pxe_info.values()), True) + @mock.patch.object(os, 'chmod', autospec=True) @mock.patch.object(pxe_utils, 'TFTPImageCache', lambda: None) @mock.patch.object(pxe_utils, 'ensure_tree', autospec=True) @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) def test_cache_ramdisk_kernel_ipxe(self, mock_fetch_image, - mock_ensure_tree): - fake_pxe_info = {'foo': 'bar'} + mock_ensure_tree, mock_chmod): + fake_pxe_info = pxe_utils.get_image_info(self.node) expected_path = os.path.join(CONF.deploy.http_root, self.node.uuid) with task_manager.acquire(self.context, self.node.uuid, @@ -1475,7 +1527,7 @@ class PXEBuildKickstartConfigOptionsTestCase(db_base.DbTestCase): image_info['ks_template'][1], {'ks_options': params} ) write_mock.assert_called_with(image_info['ks_cfg'][1], - render_mock.return_value) + render_mock.return_value, 0o644) def test_validate_kickstart_template(self): self.config_temp_dir('http_root', group='deploy') diff --git a/ironic/tests/unit/common/test_rpc_service.py b/ironic/tests/unit/common/test_rpc_service.py index 4ba3b200a..4e190f5e6 100644 --- a/ironic/tests/unit/common/test_rpc_service.py +++ b/ironic/tests/unit/common/test_rpc_service.py @@ -55,3 +55,25 @@ class TestRPCService(base.TestCase): mock_init_method.assert_called_once_with(self.rpc_svc.manager, mock_ctx.return_value) self.assertIs(rpc.GLOBAL_MANAGER, self.rpc_svc.manager) + + @mock.patch.object(manager.ConductorManager, 'prepare_host', autospec=True) + @mock.patch.object(oslo_messaging, 'Target', autospec=True) + @mock.patch.object(objects_base, 'IronicObjectSerializer', autospec=True) + @mock.patch.object(rpc, 'get_server', autospec=True) + @mock.patch.object(manager.ConductorManager, 'init_host', autospec=True) + @mock.patch.object(context, 'get_admin_context', autospec=True) + def test_start_no_rpc(self, mock_ctx, mock_init_method, + mock_rpc, mock_ios, mock_target, + mock_prepare_method): + CONF.set_override('rpc_transport', 'none') + self.rpc_svc.start() + + self.assertIsNone(self.rpc_svc.rpcserver) + mock_ctx.assert_called_once_with() + mock_target.assert_not_called() + mock_rpc.assert_not_called() + mock_ios.assert_called_once_with(is_server=True) + mock_prepare_method.assert_called_once_with(self.rpc_svc.manager) + mock_init_method.assert_called_once_with(self.rpc_svc.manager, + mock_ctx.return_value) + self.assertIs(rpc.GLOBAL_MANAGER, self.rpc_svc.manager) diff --git a/ironic/tests/unit/conductor/mgr_utils.py b/ironic/tests/unit/conductor/mgr_utils.py index 9baadaf42..4451d7a15 100644 --- a/ironic/tests/unit/conductor/mgr_utils.py +++ b/ironic/tests/unit/conductor/mgr_utils.py @@ -24,6 +24,7 @@ from oslo_utils import strutils from oslo_utils import uuidutils from ironic.common import exception +from ironic.common import pxe_utils from ironic.common import states from ironic.conductor import manager from ironic import objects @@ -143,8 +144,10 @@ class ServiceSetUpMixin(object): self.service.init_host() else: with mock.patch.object(periodics, 'PeriodicWorker', autospec=True): - self.service.prepare_host() - self.service.init_host() + with mock.patch.object(pxe_utils, 'place_common_config', + autospec=True): + self.service.prepare_host() + self.service.init_host() self.addCleanup(self._stop_service) diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py index d207bb2f7..56f67b2c2 100644 --- a/ironic/tests/unit/conductor/test_rpcapi.py +++ b/ironic/tests/unit/conductor/test_rpcapi.py @@ -77,6 +77,12 @@ class RPCAPITestCase(db_base.DbTestCase): self.context, objects.Node(), self.fake_node) self.fake_portgroup = db_utils.get_test_portgroup() + def test_rpc_disabled(self): + CONF.set_override('rpc_transport', 'none') + rpcapi = conductor_rpcapi.ConductorAPI(topic='fake-topic') + self.assertIsNone(rpcapi.client) + self.assertTrue(rpcapi._can_send_version('9.99')) + def test_serialized_instance_has_uuid(self): self.assertIn('uuid', self.fake_node) @@ -728,6 +734,17 @@ class RPCAPITestCase(db_base.DbTestCase): @mock.patch.object(rpc, 'GLOBAL_MANAGER', spec_set=conductor_manager.ConductorManager) + def test_local_call_with_rpc_disabled(self, mock_manager): + CONF.set_override('host', 'fake.host') + CONF.set_override('rpc_transport', 'none') + rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic') + rpcapi.create_node(mock.sentinel.context, mock.sentinel.node, + topic='fake.topic.fake.host') + mock_manager.create_node.assert_called_once_with( + mock.sentinel.context, node_obj=mock.sentinel.node) + + @mock.patch.object(rpc, 'GLOBAL_MANAGER', + spec_set=conductor_manager.ConductorManager) def test_local_call_host_mismatch(self, mock_manager): CONF.set_override('host', 'fake.host') rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic') @@ -740,6 +757,27 @@ class RPCAPITestCase(db_base.DbTestCase): @mock.patch.object(rpc, 'GLOBAL_MANAGER', spec_set=conductor_manager.ConductorManager) + def test_local_call_host_mismatch_with_rpc_disabled(self, mock_manager): + CONF.set_override('host', 'fake.host') + CONF.set_override('rpc_transport', 'none') + rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic') + self.assertRaises(exception.ServiceUnavailable, + rpcapi.create_node, + mock.sentinel.context, mock.sentinel.node, + topic='fake.topic.not-fake.host') + + @mock.patch.object(rpc, 'GLOBAL_MANAGER', None) + def test_local_call_no_conductor_with_rpc_disabled(self): + CONF.set_override('host', 'fake.host') + CONF.set_override('rpc_transport', 'none') + rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic') + self.assertRaises(exception.ServiceUnavailable, + rpcapi.create_node, + mock.sentinel.context, mock.sentinel.node, + topic='fake.topic.fake.host') + + @mock.patch.object(rpc, 'GLOBAL_MANAGER', + spec_set=conductor_manager.ConductorManager) def test_local_cast(self, mock_manager): CONF.set_override('host', 'fake.host') rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic') diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py index 2254a2c72..085c96c41 100644 --- a/ironic/tests/unit/drivers/modules/test_ipxe.py +++ b/ironic/tests/unit/drivers/modules/test_ipxe.py @@ -392,7 +392,7 @@ class iPXEBootTestCase(db_base.DbTestCase): os.path.join( CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) render_mock.assert_called_once_with( CONF.pxe.ipxe_boot_script, {'ipxe_for_mac_uri': 'pxelinux.cfg/', @@ -413,7 +413,7 @@ class iPXEBootTestCase(db_base.DbTestCase): os.path.join( CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) render_mock.assert_called_once_with( CONF.pxe.ipxe_boot_script, {'ipxe_for_mac_uri': 'pxelinux.cfg/', @@ -441,7 +441,7 @@ class iPXEBootTestCase(db_base.DbTestCase): os.path.join( CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) @mock.patch.object(common_utils, 'render_template', lambda *args: 'foo') @mock.patch('ironic.common.utils.write_to_file', autospec=True) @@ -455,7 +455,7 @@ class iPXEBootTestCase(db_base.DbTestCase): os.path.join( CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)), - 'foo') + 'foo', 0o644) def test_prepare_ramdisk_cleaning(self): self.node.provision_state = states.CLEANING diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index ce40b8dd8..e1559b6f6 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -862,7 +862,7 @@ class PXEBootTestCase(db_base.DbTestCase): provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) render_mock.assert_called() write_file_mock.assert_called_with( - '/path/to/ks.cfg', render_mock.return_value + '/path/to/ks.cfg', render_mock.return_value, 0o644 ) create_pxe_config_mock.assert_called_once_with( task, mock.ANY, CONF.pxe.uefi_pxe_config_template, @@ -926,7 +926,7 @@ class PXEBootTestCase(db_base.DbTestCase): provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) render_mock.assert_called() write_file_mock.assert_called_with( - '/path/to/ks.cfg', render_mock.return_value + '/path/to/ks.cfg', render_mock.return_value, 0o644 ) create_pxe_config_mock.assert_called_once_with( task, mock.ANY, CONF.pxe.pxe_config_template, diff --git a/releasenotes/notes/initial_grub-566688b16f773fcf.yaml b/releasenotes/notes/initial_grub-566688b16f773fcf.yaml new file mode 100644 index 000000000..0c8784949 --- /dev/null +++ b/releasenotes/notes/initial_grub-566688b16f773fcf.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Manually copying the initial grub config for grub network boot is no longer + necessary, as this file is now written to the TFTP root directory on + conductor startup. A custom template can be used to generate this file with + config option ``[pxe]initial_grub_template``. diff --git a/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml b/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml new file mode 100644 index 000000000..332ed241a --- /dev/null +++ b/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds a new ``none`` RPC transport that can be used together with the + combined ``ironic`` executable to completely disable the RPC bus. diff --git a/zuul.d/ironic-jobs.yaml b/zuul.d/ironic-jobs.yaml index d697d8aa6..cbf45a2f2 100644 --- a/zuul.d/ironic-jobs.yaml +++ b/zuul.d/ironic-jobs.yaml @@ -45,7 +45,7 @@ IRONIC_TEMPEST_WHOLE_DISK_IMAGE: False IRONIC_VM_COUNT: 2 IRONIC_VM_EPHEMERAL_DISK: 1 - IRONIC_VM_SPECS_RAM: 2800 + IRONIC_VM_SPECS_RAM: 2600 IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' # NOTE(dtantsur): in some jobs we end up with 12 disks total, so reduce # each of them. For don't need all 10 GiB for CirrOS anyway. |