Rescue extension for CoreOS with DHCP tenant networks
This patch adds support for rescue mode with DHCP tenant networks in CoreOS. Applying network config from a configdrive is not yet supported but will be in a future patch. Co-Authored-By: Jay Faulkner <jay@jvf.cc> Co-Authored-By: Taku Izumi <izumi.taku@jp.fujitsu.com> Co-Authored-By: Annie Lezil <annie.lezil@gmail.com> Co-Authored-By: Aparna <aparnavtce@gmail.com> Co-Authored-By: Shivanand Tendulker <stendulker@gmail.com> Change-Id: I7898ff22800dedba73d7fbfb3801378867abe183 Partial-Bug: 1526449
This commit is contained in:
parent
644f2c326f
commit
a659306272
@ -189,6 +189,41 @@ coreos:
|
||||
Type=none
|
||||
Options=bind
|
||||
|
||||
- name: setup-rescue-directories.service
|
||||
command: start
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Create directories for rescue mode configuration
|
||||
After=ironic-python-agent-container-creation.service
|
||||
Requires=ironic-python-agent-container-creation.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/mkdir /etc/ipa-rescue-config
|
||||
ExecStart=/usr/bin/mkdir /opt/ironic-python-agent/etc/ipa-rescue-config
|
||||
|
||||
- name: opt-ironic\x2dpython\x2dagent-etc-ipa\x2drescue\x2dconfig.mount
|
||||
command: start
|
||||
content: |
|
||||
[Unit]
|
||||
DefaultDependencies=no
|
||||
|
||||
Conflicts=umount.target
|
||||
Before=umount.target
|
||||
|
||||
After=ironic-python-agent-container-creation.service
|
||||
After=setup-rescue-directories.service
|
||||
|
||||
Requires=ironic-python-agent-container-creation.service
|
||||
Requires=setup-rescue-directories.service
|
||||
|
||||
[Mount]
|
||||
What=/etc/ipa-rescue-config
|
||||
Where=/opt/ironic-python-agent/etc/ipa-rescue-config
|
||||
Type=none
|
||||
Options=bind
|
||||
|
||||
- name: ironic-python-agent.service
|
||||
command: start
|
||||
content: |
|
||||
@ -203,6 +238,8 @@ coreos:
|
||||
After=opt-ironic\x2dpython\x2dagent-mnt.mount
|
||||
After=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
||||
After=opt-ironic\x2dpython\x2dagent-run-log.mount
|
||||
After=setup-rescue-directories.service
|
||||
After=opt-ironic\x2dpython\x2dagent-etc-ipa\x2drescue\x2dconfig.mount
|
||||
|
||||
Requires=ironic-python-agent-container-creation.service
|
||||
Requires=opt-ironic\x2dpython\x2dagent-proc.mount
|
||||
@ -213,6 +250,8 @@ coreos:
|
||||
Requires=opt-ironic\x2dpython\x2dagent-mnt.mount
|
||||
Requires=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
||||
Requires=opt-ironic\x2dpython\x2dagent-run-log.mount
|
||||
Requires=setup-rescue-directories.service
|
||||
Requires=opt-ironic\x2dpython\x2dagent-etc-ipa\x2drescue\x2dconfig.mount
|
||||
|
||||
[Service]
|
||||
ExecStartPre=-/usr/sbin/modprobe ipmi_msghandler
|
||||
@ -220,6 +259,6 @@ coreos:
|
||||
ExecStartPre=-/usr/sbin/modprobe ipmi_si
|
||||
ExecStart=/usr/bin/chroot /opt/ironic-python-agent \
|
||||
/usr/local/bin/ironic-python-agent
|
||||
Restart=always
|
||||
ExecStopPost=/usr/share/oem/finalize_rescue.sh
|
||||
Restart=on-failure
|
||||
RestartSec=30s
|
||||
|
||||
|
37
imagebuild/coreos/oem/finalize_rescue.sh
Executable file
37
imagebuild/coreos/oem/finalize_rescue.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
create_rescue_user() {
|
||||
echo "Adding rescue user with root privileges..."
|
||||
crypted_pass=$(</etc/ipa-rescue-config/ipa-rescue-password)
|
||||
sudo useradd -m rescue -G sudo -p $crypted_pass
|
||||
sudo echo "rescue ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/rescue
|
||||
}
|
||||
|
||||
setup_dhcp_network() {
|
||||
DHCP_CONFIG_TEMPLATE=/usr/share/oem/rescue-dhcp-config.network
|
||||
|
||||
echo "Configuring DHCP networks on all interfaces..."
|
||||
echo "Removing all existing network configuration..."
|
||||
sudo rm /etc/systemd/network/*
|
||||
|
||||
echo "Configuring all interfaces except loopback to DHCP..."
|
||||
for interface in $(ls /sys/class/net) ; do
|
||||
if [ $interface != "lo" ]; then
|
||||
sudo sed "s/RESCUE_NETWORK_INTERFACE/$interface/" $DHCP_CONFIG_TEMPLATE > /etc/systemd/network/50-$interface.network || true
|
||||
fi
|
||||
done
|
||||
|
||||
sudo systemctl restart systemd-networkd
|
||||
}
|
||||
|
||||
echo "Attempting to start rescue mode configuration..."
|
||||
if [ -f /etc/ipa-rescue-config/ipa-rescue-password ]; then
|
||||
# NOTE(mariojv) An exit code of 0 is always forced here to avoid making IPA
|
||||
# restart after something fails. IPA should not restart when this script
|
||||
# executes to avoid exposing its API to a tenant network.
|
||||
create_rescue_user || exit 0
|
||||
setup_dhcp_network || exit 0
|
||||
# TODO(mariojv) Add support for configdrive and static networks
|
||||
else
|
||||
echo "One or more of the files needed for rescue mode does not exist, not rescuing."
|
||||
fi
|
5
imagebuild/coreos/oem/rescue-dhcp-config.network
Normal file
5
imagebuild/coreos/oem/rescue-dhcp-config.network
Normal file
@ -0,0 +1,5 @@
|
||||
[Match]
|
||||
Name=RESCUE_NETWORK_INTERFACE
|
||||
|
||||
[Network]
|
||||
DHCP=yes
|
@ -192,6 +192,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
||||
self.network_interface = network_interface
|
||||
self.standalone = standalone
|
||||
self.hardware_initialization_delay = hardware_initialization_delay
|
||||
# IPA will stop serving requests and exit after this is set to False
|
||||
self.serve_api = True
|
||||
|
||||
def get_status(self):
|
||||
"""Retrieve a serializable status.
|
||||
@ -316,6 +318,30 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
||||
LOG.warning("No valid network interfaces found. "
|
||||
"Node lookup will probably fail.")
|
||||
|
||||
def serve_ipa_api(self):
|
||||
"""Serve the API until an extension terminates it."""
|
||||
if netutils.is_ipv6_enabled():
|
||||
# Listens to both IP versions, assuming IPV6_V6ONLY isn't enabled,
|
||||
# (the default behaviour in linux)
|
||||
simple_server.WSGIServer.address_family = socket.AF_INET6
|
||||
server = simple_server.WSGIServer((self.listen_address.hostname,
|
||||
self.listen_address.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
server.set_app(self.api)
|
||||
|
||||
if not self.standalone and self.api_url:
|
||||
# Don't start heartbeating until the server is listening
|
||||
self.heartbeater.start()
|
||||
|
||||
while self.serve_api:
|
||||
try:
|
||||
server.handle_request()
|
||||
except BaseException as e:
|
||||
msg = "Failed due to unknow exception. Error %s" % e
|
||||
LOG.exception(msg)
|
||||
raise errors.IronicAPIError(msg)
|
||||
LOG.info('shutting down')
|
||||
|
||||
def run(self):
|
||||
"""Run the Ironic Python Agent."""
|
||||
# Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError
|
||||
@ -369,24 +395,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
||||
LOG.error('Neither ipa-api-url nor inspection_callback_url'
|
||||
'found, please check your pxe append parameters.')
|
||||
|
||||
if netutils.is_ipv6_enabled():
|
||||
# Listens to both IP versions, assuming IPV6_V6ONLY isn't enabled,
|
||||
# (the default behaviour in linux)
|
||||
simple_server.WSGIServer.address_family = socket.AF_INET6
|
||||
wsgi = simple_server.make_server(
|
||||
self.listen_address.hostname,
|
||||
self.listen_address.port,
|
||||
self.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
|
||||
if not self.standalone and self.api_url:
|
||||
# Don't start heartbeating until the server is listening
|
||||
self.heartbeater.start()
|
||||
|
||||
try:
|
||||
wsgi.serve_forever()
|
||||
except BaseException:
|
||||
LOG.exception('shutting down')
|
||||
self.serve_ipa_api()
|
||||
|
||||
if not self.standalone and self.api_url:
|
||||
self.heartbeater.stop()
|
||||
|
62
ironic_python_agent/extensions/rescue.py
Normal file
62
ironic_python_agent/extensions/rescue.py
Normal file
@ -0,0 +1,62 @@
|
||||
# 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 crypt
|
||||
import random
|
||||
import string
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from ironic_python_agent.extensions import base
|
||||
|
||||
LOG = log.getLogger()
|
||||
|
||||
PASSWORD_FILE = '/etc/ipa-rescue-config/ipa-rescue-password'
|
||||
|
||||
|
||||
class RescueExtension(base.BaseAgentExtension):
|
||||
|
||||
def make_salt(self):
|
||||
"""Generate a random salt for hashing the rescue password.
|
||||
|
||||
Salt should be a two-character string from the set [a-zA-Z0-9].
|
||||
|
||||
:returns: a valid salt for use with crypt.crypt
|
||||
"""
|
||||
allowed_chars = string.ascii_letters + string.digits
|
||||
return random.choice(allowed_chars) + random.choice(allowed_chars)
|
||||
|
||||
def write_rescue_password(self, rescue_password=""):
|
||||
"""Write rescue password to a file for use after IPA exits.
|
||||
|
||||
:param rescue_password: Rescue password.
|
||||
"""
|
||||
LOG.debug('Writing hashed rescue password to %s', PASSWORD_FILE)
|
||||
salt = self.make_salt()
|
||||
hashed_password = crypt.crypt(rescue_password, salt)
|
||||
try:
|
||||
with open(PASSWORD_FILE, 'w') as f:
|
||||
f.write(hashed_password)
|
||||
except IOError as e:
|
||||
msg = ("Rescue Operation failed when writing the hashed rescue "
|
||||
"password to the password file. Error %s") % e
|
||||
LOG.exception(msg)
|
||||
raise IOError(msg)
|
||||
|
||||
@base.sync_command('finalize_rescue')
|
||||
def finalize_rescue(self, rescue_password=""):
|
||||
"""Sets the rescue password for the rescue user."""
|
||||
self.write_rescue_password(rescue_password)
|
||||
# IPA will terminate after the result of finalize_rescue is returned to
|
||||
# ironic to avoid exposing the IPA API to a tenant or public network
|
||||
self.agent.serve_api = False
|
||||
return
|
77
ironic_python_agent/tests/unit/extensions/test_rescue.py
Normal file
77
ironic_python_agent/tests/unit/extensions/test_rescue.py
Normal file
@ -0,0 +1,77 @@
|
||||
# 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 string
|
||||
|
||||
import mock
|
||||
from oslotest import base as test_base
|
||||
|
||||
from ironic_python_agent.extensions import rescue
|
||||
from ironic_python_agent.tests.unit.extensions.test_base import FakeAgent
|
||||
|
||||
|
||||
class TestRescueExtension(test_base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRescueExtension, self).setUp()
|
||||
self.agent_extension = rescue.RescueExtension()
|
||||
self.agent_extension.agent = FakeAgent()
|
||||
|
||||
def test_make_salt(self):
|
||||
salt = self.agent_extension.make_salt()
|
||||
self.assertEqual(2, len(salt))
|
||||
for char in salt:
|
||||
self.assertIn(char, string.ascii_letters + string.digits)
|
||||
|
||||
@mock.patch('ironic_python_agent.extensions.rescue.crypt.crypt',
|
||||
autospec=True)
|
||||
@mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.'
|
||||
'make_salt', autospec=True)
|
||||
def test_write_rescue_password(self, mock_salt, mock_crypt):
|
||||
mock_salt.return_value = '12'
|
||||
mock_crypt.return_value = '12deadbeef'
|
||||
mock_open = mock.mock_open()
|
||||
with mock.patch('ironic_python_agent.extensions.rescue.open',
|
||||
mock_open):
|
||||
self.agent_extension.write_rescue_password('password')
|
||||
|
||||
mock_crypt.assert_called_once_with('password', '12')
|
||||
mock_open.assert_called_once_with(
|
||||
'/etc/ipa-rescue-config/ipa-rescue-password', 'w')
|
||||
file_handle = mock_open()
|
||||
file_handle.write.assert_called_once_with('12deadbeef')
|
||||
|
||||
@mock.patch('ironic_python_agent.extensions.rescue.crypt.crypt',
|
||||
autospec=True)
|
||||
@mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.'
|
||||
'make_salt', autospec=True)
|
||||
def test_write_rescue_password_ioerror(self, mock_salt, mock_crypt):
|
||||
mock_salt.return_value = '12'
|
||||
mock_crypt.return_value = '12deadbeef'
|
||||
mock_open = mock.mock_open()
|
||||
with mock.patch('ironic_python_agent.extensions.rescue.open',
|
||||
mock_open):
|
||||
mock_open.side_effect = IOError
|
||||
# Make sure IOError gets reraised for caller to handle
|
||||
self.assertRaises(
|
||||
IOError, self.agent_extension.write_rescue_password,
|
||||
'password')
|
||||
|
||||
@mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.'
|
||||
'write_rescue_password', autospec=True)
|
||||
def test_finalize_rescue(self, mock_write_rescue_password):
|
||||
self.agent_extension.agent.serve_api = True
|
||||
self.agent_extension.finalize_rescue(rescue_password='password')
|
||||
mock_write_rescue_password.assert_called_once_with(
|
||||
mock.ANY,
|
||||
rescue_password='password')
|
||||
self.assertFalse(self.agent_extension.agent.serve_api)
|
@ -176,15 +176,21 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
@mock.patch(
|
||||
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||
mock.Mock())
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch.object(agent.IronicPythonAgent,
|
||||
'_wait_for_interface', autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
def test_run(self, mock_make_server, mock_dispatch, mock_wait):
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware, 'load_managers', autospec=True)
|
||||
def test_run(self, mock_load_managers, mock_wsgi,
|
||||
mock_wait, mock_dispatch):
|
||||
CONF.set_override('inspection_callback_url', '')
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
|
||||
wsgi_server = mock_wsgi.return_value
|
||||
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server.handle_request.side_effect = set_serve_api
|
||||
self.agent.heartbeater = mock.Mock()
|
||||
self.agent.api_client.lookup_node = mock.Mock()
|
||||
self.agent.api_client.lookup_node.return_value = {
|
||||
@ -195,21 +201,66 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
'heartbeat_timeout': 300
|
||||
}
|
||||
}
|
||||
|
||||
self.agent.run()
|
||||
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
wsgi_server.set_app.assert_called_once_with(self.agent.api)
|
||||
self.assertTrue(wsgi_server.handle_request.called)
|
||||
mock_wait.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual([mock.call('list_hardware_info'),
|
||||
mock.call('wait_for_disks')],
|
||||
mock_dispatch.call_args_list)
|
||||
self.agent.heartbeater.start.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(hardware, '_check_for_iscsi', mock.Mock())
|
||||
@mock.patch(
|
||||
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||
mock.Mock())
|
||||
@mock.patch.object(agent.IronicPythonAgent,
|
||||
'_wait_for_interface', autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware, 'load_managers', autospec=True)
|
||||
def test_run_raise_exception(self, mock_load_managers, mock_wsgi,
|
||||
mock_dispatch, mock_wait):
|
||||
CONF.set_override('inspection_callback_url', '')
|
||||
|
||||
wsgi_server = mock_wsgi.return_value
|
||||
wsgi_server.handle_request.side_effect = KeyboardInterrupt()
|
||||
self.agent.heartbeater = mock.Mock()
|
||||
self.agent.api_client.lookup_node = mock.Mock()
|
||||
self.agent.api_client.lookup_node.return_value = {
|
||||
'node': {
|
||||
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
|
||||
},
|
||||
'config': {
|
||||
'heartbeat_timeout': 300
|
||||
}
|
||||
}
|
||||
|
||||
self.assertRaisesRegex(errors.IronicAPIError,
|
||||
'Failed due to unknow exception.',
|
||||
self.agent.run)
|
||||
|
||||
self.assertTrue(mock_wait.called)
|
||||
self.assertEqual([mock.call('list_hardware_info'),
|
||||
mock.call('wait_for_disks')],
|
||||
mock_dispatch.call_args_list)
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
wsgi_server.set_app.assert_called_once_with(self.agent.api)
|
||||
self.assertTrue(wsgi_server.handle_request.called)
|
||||
self.agent.heartbeater.start.assert_called_once_with()
|
||||
self.assertTrue(wsgi_server.handle_request.called)
|
||||
|
||||
@mock.patch.object(hardware, '_check_for_iscsi', mock.Mock())
|
||||
@mock.patch('ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||
mock.Mock())
|
||||
@ -217,15 +268,18 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
'_wait_for_interface', autospec=True)
|
||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||
autospec=True)
|
||||
def test_run_with_inspection(self, mock_list_hardware, mock_make_server,
|
||||
def test_run_with_inspection(self, mock_list_hardware, mock_wsgi,
|
||||
mock_dispatch, mock_inspector, mock_wait):
|
||||
CONF.set_override('inspection_callback_url', 'http://foo/bar')
|
||||
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server = mock_wsgi.return_value
|
||||
wsgi_server.handle_request.side_effect = set_serve_api
|
||||
|
||||
mock_inspector.return_value = 'uuid'
|
||||
|
||||
@ -242,12 +296,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
self.agent.run()
|
||||
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
self.assertTrue(mock_wsgi.called)
|
||||
|
||||
mock_inspector.assert_called_once_with()
|
||||
self.assertEqual(1, self.agent.api_client.lookup_node.call_count)
|
||||
self.assertEqual(
|
||||
@ -268,12 +322,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
'_wait_for_interface', autospec=True)
|
||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||
autospec=True)
|
||||
def test_run_with_inspection_without_apiurl(self,
|
||||
mock_list_hardware,
|
||||
mock_make_server,
|
||||
mock_wsgi,
|
||||
mock_dispatch,
|
||||
mock_inspector,
|
||||
mock_wait):
|
||||
@ -295,18 +349,21 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
self.assertFalse(hasattr(self.agent, 'api_client'))
|
||||
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
||||
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server = mock_wsgi.return_value
|
||||
wsgi_server.handle_request.side_effect = set_serve_api
|
||||
|
||||
self.agent.run()
|
||||
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
self.assertTrue(wsgi_server.handle_request.called)
|
||||
|
||||
mock_inspector.assert_called_once_with()
|
||||
|
||||
self.assertFalse(mock_wait.called)
|
||||
@ -320,12 +377,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
'_wait_for_interface', autospec=True)
|
||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||
autospec=True)
|
||||
def test_run_without_inspection_and_apiurl(self,
|
||||
mock_list_hardware,
|
||||
mock_make_server,
|
||||
mock_wsgi,
|
||||
mock_dispatch,
|
||||
mock_inspector,
|
||||
mock_wait):
|
||||
@ -347,18 +404,20 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
self.assertFalse(hasattr(self.agent, 'api_client'))
|
||||
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
||||
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server = mock_wsgi.return_value
|
||||
wsgi_server.handle_request.side_effect = set_serve_api
|
||||
|
||||
self.agent.run()
|
||||
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
self.assertTrue(wsgi_server.handle_request.called)
|
||||
|
||||
self.assertFalse(mock_inspector.called)
|
||||
self.assertFalse(mock_wait.called)
|
||||
@ -392,12 +451,16 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
@mock.patch.object(agent.IronicPythonAgent, '_wait_for_interface',
|
||||
autospec=True)
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
def test_run_with_sleep(self, mock_make_server, mock_dispatch,
|
||||
mock_wait, mock_sleep, mock_load_managers):
|
||||
CONF.set_override('inspection_callback_url', '')
|
||||
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
wsgi_server.handle_request.side_effect = set_serve_api
|
||||
|
||||
self.agent.hardware_initialization_delay = 10
|
||||
self.agent.heartbeater = mock.Mock()
|
||||
@ -414,11 +477,9 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
||||
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
|
||||
self.agent.heartbeater.start.assert_called_once_with()
|
||||
mock_sleep.assert_called_once_with(10)
|
||||
@ -539,32 +600,35 @@ class TestAgentStandalone(ironic_agent_base.IronicAgentTest):
|
||||
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||
mock.Mock())
|
||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||
autospec=True)
|
||||
def test_run(self, mock_list_hardware, mock_make_server):
|
||||
@mock.patch.object(hardware, 'load_managers', autospec=True)
|
||||
def test_run(self, mock_load_managers, mock_list_hardware,
|
||||
mock_wsgi, mock_make_server):
|
||||
wsgi_server = mock_make_server.return_value
|
||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||
wsgi_server_request = mock_wsgi.return_value
|
||||
|
||||
def set_serve_api():
|
||||
self.agent.serve_api = False
|
||||
|
||||
wsgi_server_request.handle_request.side_effect = set_serve_api
|
||||
|
||||
self.agent.heartbeater = mock.Mock()
|
||||
self.agent.api_client.lookup_node = mock.Mock()
|
||||
self.agent.api_client.lookup_node.return_value = {
|
||||
'node': {
|
||||
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
|
||||
},
|
||||
'config': {
|
||||
'heartbeat_timeout': 300
|
||||
}
|
||||
}
|
||||
|
||||
self.agent.run()
|
||||
|
||||
self.assertTrue(mock_load_managers.called)
|
||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||
mock_make_server.assert_called_once_with(
|
||||
listen_addr.hostname,
|
||||
listen_addr.port,
|
||||
self.agent.api,
|
||||
server_class=simple_server.WSGIServer)
|
||||
wsgi_server.serve_forever.assert_called_once_with()
|
||||
mock_wsgi.assert_called_once_with(
|
||||
(listen_addr.hostname,
|
||||
listen_addr.port),
|
||||
simple_server.WSGIRequestHandler)
|
||||
wsgi_server_request.set_app.assert_called_once_with(self.agent.api)
|
||||
|
||||
self.assertTrue(wsgi_server_request.handle_request.called)
|
||||
self.assertFalse(self.agent.heartbeater.called)
|
||||
self.assertFalse(self.agent.api_client.lookup_node.called)
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Adds an extension to support rescue mode for CoreOS agents using DHCP
|
||||
for the tenant network.
|
@ -29,6 +29,7 @@ ironic_python_agent.extensions =
|
||||
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||
image = ironic_python_agent.extensions.image:ImageExtension
|
||||
log = ironic_python_agent.extensions.log:LogExtension
|
||||
rescue = ironic_python_agent.extensions.rescue:RescueExtension
|
||||
|
||||
ironic_python_agent.hardware_managers =
|
||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||
|
Loading…
Reference in New Issue
Block a user