summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devstack/lib/ironic22
-rw-r--r--doc/source/install/configure-pxe.rst32
-rw-r--r--doc/source/install/standalone/configure.rst14
-rw-r--r--ironic/api/wsgi.py1
-rw-r--r--ironic/cmd/api.py1
-rw-r--r--ironic/cmd/conductor.py1
-rw-r--r--ironic/common/pxe_utils.py38
-rw-r--r--ironic/common/rpc_service.py13
-rw-r--r--ironic/common/service.py8
-rw-r--r--ironic/common/utils.py4
-rw-r--r--ironic/conductor/base_manager.py11
-rw-r--r--ironic/conductor/rpcapi.py33
-rw-r--r--ironic/conf/default.py3
-rw-r--r--ironic/conf/pxe.py5
-rw-r--r--ironic/drivers/modules/initial_grub_cfg.template7
-rw-r--r--ironic/drivers/modules/ipxe.py4
-rw-r--r--ironic/drivers/modules/master_grub_cfg.txt7
-rw-r--r--ironic/drivers/modules/pxe.py7
-rw-r--r--ironic/tests/base.py1
-rw-r--r--ironic/tests/unit/common/test_pxe_utils.py84
-rw-r--r--ironic/tests/unit/common/test_rpc_service.py22
-rw-r--r--ironic/tests/unit/conductor/mgr_utils.py7
-rw-r--r--ironic/tests/unit/conductor/test_rpcapi.py38
-rw-r--r--ironic/tests/unit/drivers/modules/test_ipxe.py8
-rw-r--r--ironic/tests/unit/drivers/modules/test_pxe.py4
-rw-r--r--releasenotes/notes/initial_grub-566688b16f773fcf.yaml7
-rw-r--r--releasenotes/notes/rpc-none-f05dac657eef4b66.yaml5
-rw-r--r--zuul.d/ironic-jobs.yaml2
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.