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
|
Type=none
|
||||||
Options=bind
|
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
|
- name: ironic-python-agent.service
|
||||||
command: start
|
command: start
|
||||||
content: |
|
content: |
|
||||||
@ -203,6 +238,8 @@ coreos:
|
|||||||
After=opt-ironic\x2dpython\x2dagent-mnt.mount
|
After=opt-ironic\x2dpython\x2dagent-mnt.mount
|
||||||
After=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
After=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
||||||
After=opt-ironic\x2dpython\x2dagent-run-log.mount
|
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=ironic-python-agent-container-creation.service
|
||||||
Requires=opt-ironic\x2dpython\x2dagent-proc.mount
|
Requires=opt-ironic\x2dpython\x2dagent-proc.mount
|
||||||
@ -213,6 +250,8 @@ coreos:
|
|||||||
Requires=opt-ironic\x2dpython\x2dagent-mnt.mount
|
Requires=opt-ironic\x2dpython\x2dagent-mnt.mount
|
||||||
Requires=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
Requires=opt-ironic\x2dpython\x2dagent-etc-resolvconf.service
|
||||||
Requires=opt-ironic\x2dpython\x2dagent-run-log.mount
|
Requires=opt-ironic\x2dpython\x2dagent-run-log.mount
|
||||||
|
Requires=setup-rescue-directories.service
|
||||||
|
Requires=opt-ironic\x2dpython\x2dagent-etc-ipa\x2drescue\x2dconfig.mount
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStartPre=-/usr/sbin/modprobe ipmi_msghandler
|
ExecStartPre=-/usr/sbin/modprobe ipmi_msghandler
|
||||||
@ -220,6 +259,6 @@ coreos:
|
|||||||
ExecStartPre=-/usr/sbin/modprobe ipmi_si
|
ExecStartPre=-/usr/sbin/modprobe ipmi_si
|
||||||
ExecStart=/usr/bin/chroot /opt/ironic-python-agent \
|
ExecStart=/usr/bin/chroot /opt/ironic-python-agent \
|
||||||
/usr/local/bin/ironic-python-agent
|
/usr/local/bin/ironic-python-agent
|
||||||
Restart=always
|
ExecStopPost=/usr/share/oem/finalize_rescue.sh
|
||||||
|
Restart=on-failure
|
||||||
RestartSec=30s
|
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.network_interface = network_interface
|
||||||
self.standalone = standalone
|
self.standalone = standalone
|
||||||
self.hardware_initialization_delay = hardware_initialization_delay
|
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):
|
def get_status(self):
|
||||||
"""Retrieve a serializable status.
|
"""Retrieve a serializable status.
|
||||||
@ -316,6 +318,30 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
LOG.warning("No valid network interfaces found. "
|
LOG.warning("No valid network interfaces found. "
|
||||||
"Node lookup will probably fail.")
|
"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):
|
def run(self):
|
||||||
"""Run the Ironic Python Agent."""
|
"""Run the Ironic Python Agent."""
|
||||||
# Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError
|
# 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'
|
LOG.error('Neither ipa-api-url nor inspection_callback_url'
|
||||||
'found, please check your pxe append parameters.')
|
'found, please check your pxe append parameters.')
|
||||||
|
|
||||||
if netutils.is_ipv6_enabled():
|
self.serve_ipa_api()
|
||||||
# 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')
|
|
||||||
|
|
||||||
if not self.standalone and self.api_url:
|
if not self.standalone and self.api_url:
|
||||||
self.heartbeater.stop()
|
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(
|
@mock.patch(
|
||||||
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||||
mock.Mock())
|
mock.Mock())
|
||||||
|
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
||||||
@mock.patch.object(agent.IronicPythonAgent,
|
@mock.patch.object(agent.IronicPythonAgent,
|
||||||
'_wait_for_interface', autospec=True)
|
'_wait_for_interface', autospec=True)
|
||||||
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
|
@mock.patch('wsgiref.simple_server.WSGIServer', autospec=True)
|
||||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
@mock.patch.object(hardware, 'load_managers', autospec=True)
|
||||||
def test_run(self, mock_make_server, mock_dispatch, mock_wait):
|
def test_run(self, mock_load_managers, mock_wsgi,
|
||||||
|
mock_wait, mock_dispatch):
|
||||||
CONF.set_override('inspection_callback_url', '')
|
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.heartbeater = mock.Mock()
|
||||||
self.agent.api_client.lookup_node = mock.Mock()
|
self.agent.api_client.lookup_node = mock.Mock()
|
||||||
self.agent.api_client.lookup_node.return_value = {
|
self.agent.api_client.lookup_node.return_value = {
|
||||||
@ -195,21 +201,66 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
'heartbeat_timeout': 300
|
'heartbeat_timeout': 300
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.agent.run()
|
self.agent.run()
|
||||||
|
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_wsgi.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
wsgi_server.set_app.assert_called_once_with(self.agent.api)
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
self.assertTrue(wsgi_server.handle_request.called)
|
||||||
mock_wait.assert_called_once_with(mock.ANY)
|
mock_wait.assert_called_once_with(mock.ANY)
|
||||||
self.assertEqual([mock.call('list_hardware_info'),
|
self.assertEqual([mock.call('list_hardware_info'),
|
||||||
mock.call('wait_for_disks')],
|
mock.call('wait_for_disks')],
|
||||||
mock_dispatch.call_args_list)
|
mock_dispatch.call_args_list)
|
||||||
self.agent.heartbeater.start.assert_called_once_with()
|
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.object(hardware, '_check_for_iscsi', mock.Mock())
|
||||||
@mock.patch('ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
@mock.patch('ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||||
mock.Mock())
|
mock.Mock())
|
||||||
@ -217,15 +268,18 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
'_wait_for_interface', autospec=True)
|
'_wait_for_interface', autospec=True)
|
||||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||||
@mock.patch.object(hardware, 'dispatch_to_managers', 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',
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||||
autospec=True)
|
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):
|
mock_dispatch, mock_inspector, mock_wait):
|
||||||
CONF.set_override('inspection_callback_url', 'http://foo/bar')
|
CONF.set_override('inspection_callback_url', 'http://foo/bar')
|
||||||
|
|
||||||
wsgi_server = mock_make_server.return_value
|
def set_serve_api():
|
||||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
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'
|
mock_inspector.return_value = 'uuid'
|
||||||
|
|
||||||
@ -242,12 +296,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
self.agent.run()
|
self.agent.run()
|
||||||
|
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_wsgi.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
self.assertTrue(mock_wsgi.called)
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
|
||||||
mock_inspector.assert_called_once_with()
|
mock_inspector.assert_called_once_with()
|
||||||
self.assertEqual(1, self.agent.api_client.lookup_node.call_count)
|
self.assertEqual(1, self.agent.api_client.lookup_node.call_count)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -268,12 +322,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
'_wait_for_interface', autospec=True)
|
'_wait_for_interface', autospec=True)
|
||||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||||
@mock.patch.object(hardware, 'dispatch_to_managers', 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',
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_run_with_inspection_without_apiurl(self,
|
def test_run_with_inspection_without_apiurl(self,
|
||||||
mock_list_hardware,
|
mock_list_hardware,
|
||||||
mock_make_server,
|
mock_wsgi,
|
||||||
mock_dispatch,
|
mock_dispatch,
|
||||||
mock_inspector,
|
mock_inspector,
|
||||||
mock_wait):
|
mock_wait):
|
||||||
@ -295,18 +349,21 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
self.assertFalse(hasattr(self.agent, 'api_client'))
|
self.assertFalse(hasattr(self.agent, 'api_client'))
|
||||||
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
||||||
|
|
||||||
wsgi_server = mock_make_server.return_value
|
def set_serve_api():
|
||||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
self.agent.serve_api = False
|
||||||
|
|
||||||
|
wsgi_server = mock_wsgi.return_value
|
||||||
|
wsgi_server.handle_request.side_effect = set_serve_api
|
||||||
|
|
||||||
self.agent.run()
|
self.agent.run()
|
||||||
|
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_wsgi.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
self.assertTrue(wsgi_server.handle_request.called)
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
|
||||||
mock_inspector.assert_called_once_with()
|
mock_inspector.assert_called_once_with()
|
||||||
|
|
||||||
self.assertFalse(mock_wait.called)
|
self.assertFalse(mock_wait.called)
|
||||||
@ -320,12 +377,12 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
'_wait_for_interface', autospec=True)
|
'_wait_for_interface', autospec=True)
|
||||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||||
@mock.patch.object(hardware, 'dispatch_to_managers', 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',
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_run_without_inspection_and_apiurl(self,
|
def test_run_without_inspection_and_apiurl(self,
|
||||||
mock_list_hardware,
|
mock_list_hardware,
|
||||||
mock_make_server,
|
mock_wsgi,
|
||||||
mock_dispatch,
|
mock_dispatch,
|
||||||
mock_inspector,
|
mock_inspector,
|
||||||
mock_wait):
|
mock_wait):
|
||||||
@ -347,18 +404,20 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
self.assertFalse(hasattr(self.agent, 'api_client'))
|
self.assertFalse(hasattr(self.agent, 'api_client'))
|
||||||
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
self.assertFalse(hasattr(self.agent, 'heartbeater'))
|
||||||
|
|
||||||
wsgi_server = mock_make_server.return_value
|
def set_serve_api():
|
||||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
self.agent.serve_api = False
|
||||||
|
|
||||||
|
wsgi_server = mock_wsgi.return_value
|
||||||
|
wsgi_server.handle_request.side_effect = set_serve_api
|
||||||
|
|
||||||
self.agent.run()
|
self.agent.run()
|
||||||
|
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_wsgi.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
self.assertTrue(wsgi_server.handle_request.called)
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
|
||||||
|
|
||||||
self.assertFalse(mock_inspector.called)
|
self.assertFalse(mock_inspector.called)
|
||||||
self.assertFalse(mock_wait.called)
|
self.assertFalse(mock_wait.called)
|
||||||
@ -392,12 +451,16 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
@mock.patch.object(agent.IronicPythonAgent, '_wait_for_interface',
|
@mock.patch.object(agent.IronicPythonAgent, '_wait_for_interface',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(hardware, 'dispatch_to_managers', 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,
|
def test_run_with_sleep(self, mock_make_server, mock_dispatch,
|
||||||
mock_wait, mock_sleep, mock_load_managers):
|
mock_wait, mock_sleep, mock_load_managers):
|
||||||
CONF.set_override('inspection_callback_url', '')
|
CONF.set_override('inspection_callback_url', '')
|
||||||
|
|
||||||
|
def set_serve_api():
|
||||||
|
self.agent.serve_api = False
|
||||||
|
|
||||||
wsgi_server = mock_make_server.return_value
|
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.hardware_initialization_delay = 10
|
||||||
self.agent.heartbeater = mock.Mock()
|
self.agent.heartbeater = mock.Mock()
|
||||||
@ -414,11 +477,9 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
|
|||||||
|
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_make_server.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
|
||||||
|
|
||||||
self.agent.heartbeater.start.assert_called_once_with()
|
self.agent.heartbeater.start.assert_called_once_with()
|
||||||
mock_sleep.assert_called_once_with(10)
|
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',
|
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
|
||||||
mock.Mock())
|
mock.Mock())
|
||||||
@mock.patch('wsgiref.simple_server.make_server', 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',
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info',
|
||||||
autospec=True)
|
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 = mock_make_server.return_value
|
||||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
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.heartbeater = mock.Mock()
|
||||||
self.agent.api_client.lookup_node = 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.agent.run()
|
||||||
|
|
||||||
|
self.assertTrue(mock_load_managers.called)
|
||||||
listen_addr = agent.Host('192.0.2.1', 9999)
|
listen_addr = agent.Host('192.0.2.1', 9999)
|
||||||
mock_make_server.assert_called_once_with(
|
mock_wsgi.assert_called_once_with(
|
||||||
listen_addr.hostname,
|
(listen_addr.hostname,
|
||||||
listen_addr.port,
|
listen_addr.port),
|
||||||
self.agent.api,
|
simple_server.WSGIRequestHandler)
|
||||||
server_class=simple_server.WSGIServer)
|
wsgi_server_request.set_app.assert_called_once_with(self.agent.api)
|
||||||
wsgi_server.serve_forever.assert_called_once_with()
|
|
||||||
|
|
||||||
|
self.assertTrue(wsgi_server_request.handle_request.called)
|
||||||
self.assertFalse(self.agent.heartbeater.called)
|
self.assertFalse(self.agent.heartbeater.called)
|
||||||
self.assertFalse(self.agent.api_client.lookup_node.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
|
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||||
image = ironic_python_agent.extensions.image:ImageExtension
|
image = ironic_python_agent.extensions.image:ImageExtension
|
||||||
log = ironic_python_agent.extensions.log:LogExtension
|
log = ironic_python_agent.extensions.log:LogExtension
|
||||||
|
rescue = ironic_python_agent.extensions.rescue:RescueExtension
|
||||||
|
|
||||||
ironic_python_agent.hardware_managers =
|
ironic_python_agent.hardware_managers =
|
||||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||||
|
Loading…
Reference in New Issue
Block a user