Add iscsi extension
This extension allows IPA to be used with the PXE/iSCSI methodology of deployment in Ironic. Change-Id: I32ec9fa74182c0d03c7ef1b698b1d0c0e3007773
This commit is contained in:
parent
f1dec5c2e2
commit
d3aa7c93aa
@ -11,7 +11,7 @@ RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install -y --no-install-recommends python2.7 python2.7-dev \
|
||||
python-pip qemu-utils parted hdparm util-linux genisoimage git gcc \
|
||||
bash coreutils && \
|
||||
bash coreutils tgt && \
|
||||
apt-get -y autoremove && \
|
||||
apt-get clean
|
||||
|
||||
@ -24,7 +24,7 @@ RUN pip install /tmp/ironic-python-agent
|
||||
RUN rm -rf /tmp/ironic-python-agent
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get -y purge perl gcc-4.6 gcc python2.7-dev git python3 \
|
||||
RUN apt-get -y purge gcc-4.6 gcc python2.7-dev git python3 \
|
||||
python3-minimal python3.4 python3.4-minimal && \
|
||||
apt-get -y autoremove && \
|
||||
apt-get clean
|
||||
|
@ -282,3 +282,15 @@ class IncompatibleHardwareMethodError(RESTError):
|
||||
else:
|
||||
details = self.message
|
||||
super(IncompatibleHardwareMethodError, self).__init__(details)
|
||||
|
||||
|
||||
class ISCSIError(RESTError):
|
||||
"""Error raised when an image cannot be written to a device."""
|
||||
|
||||
message = 'Error starting iSCSI target.'
|
||||
|
||||
def __init__(self, error_msg, exit_code, stdout, stderr):
|
||||
details = ('Error starting iSCSI target: {0}. Failed with exit code '
|
||||
'{1}. stdout: {2}. stderr: {3}')
|
||||
details = details.format(error_msg, exit_code, stdout, stderr)
|
||||
super(ISCSIError, self).__init__(details)
|
||||
|
90
ironic_python_agent/extensions/iscsi.py
Normal file
90
ironic_python_agent/extensions/iscsi.py
Normal file
@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 os
|
||||
import time
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic_python_agent import errors
|
||||
from ironic_python_agent.extensions import base
|
||||
from ironic_python_agent import hardware
|
||||
from ironic_python_agent.openstack.common import log
|
||||
from ironic_python_agent import utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def _execute(cmd, error_msg, check_exit_code=None):
|
||||
if check_exit_code is None:
|
||||
check_exit_code = [0]
|
||||
|
||||
try:
|
||||
stdout, stderr = utils.execute(*cmd, check_exit_code=check_exit_code)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
LOG.error(error_msg)
|
||||
raise errors.ISCSIError(error_msg, e.exit_code, e.stdout, e.stderr)
|
||||
|
||||
|
||||
def _wait_for_iscsi_daemon(interval=1, attempts=10):
|
||||
"""Wait for the ISCSI daemon to start."""
|
||||
for attempt in range(attempts):
|
||||
if os.path.exists("/var/run/tgtd.ipc_abstract_namespace.0"):
|
||||
break
|
||||
time.sleep(interval)
|
||||
else:
|
||||
error_msg = "ISCSI daemon didn't initialize"
|
||||
LOG.error(error_msg)
|
||||
raise errors.ISCSIError(error_msg, 1, '', error_msg)
|
||||
|
||||
|
||||
def _start_iscsi_daemon(iqn, device):
|
||||
"""Start a ISCSI target for the device."""
|
||||
LOG.debug("Starting ISCSI target on device %{device}", {'device': device})
|
||||
|
||||
# Start ISCSI Target daemon
|
||||
_execute(['tgtd'], "Unable to start the ISCSI daemon")
|
||||
|
||||
_wait_for_iscsi_daemon()
|
||||
|
||||
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
|
||||
'new', '--tid', '1', '--targetname', iqn]
|
||||
_execute(cmd, "Error when adding a new target for iqn %s" % iqn)
|
||||
|
||||
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'logicalunit', '--op',
|
||||
'new', '--tid', '1', '--lun', '1', '--backing-store', device]
|
||||
_execute(cmd, "Error when adding a new logical unit for iqn %s" % iqn)
|
||||
|
||||
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
|
||||
'bind', '--tid', '1', '--initiator-address', 'ALL']
|
||||
_execute(cmd, "Error when enabling the target to accept the specific "
|
||||
"initiators for iqn %s" % iqn)
|
||||
|
||||
|
||||
class ISCSIExtension(base.BaseAgentExtension):
|
||||
|
||||
@base.sync_command('start_iscsi_target')
|
||||
def start_iscsi_target(self, iqn=None):
|
||||
"""Expose the disk as an ISCSI target."""
|
||||
# If iqn is not given, generate one
|
||||
if iqn is None:
|
||||
iqn = 'iqn-' + uuidutils.generate_uuid()
|
||||
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
_start_iscsi_daemon(iqn, device)
|
||||
return {"iscsi_target_iqn": iqn}
|
93
ironic_python_agent/tests/extensions/iscsi.py
Normal file
93
ironic_python_agent/tests/extensions/iscsi.py
Normal file
@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock
|
||||
import time
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslotest import base as test_base
|
||||
|
||||
from ironic_python_agent import errors
|
||||
from ironic_python_agent.extensions import iscsi
|
||||
from ironic_python_agent import hardware
|
||||
from ironic_python_agent import utils
|
||||
|
||||
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers')
|
||||
@mock.patch.object(utils, 'execute')
|
||||
@mock.patch.object(time, 'sleep', lambda *_: None)
|
||||
class TestISCSIExtension(test_base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestISCSIExtension, self).setUp()
|
||||
self.agent_extension = iscsi.ISCSIExtension()
|
||||
self.fake_dev = '/dev/fake'
|
||||
self.fake_iqn = 'iqn-fake'
|
||||
|
||||
@mock.patch.object(iscsi, '_wait_for_iscsi_daemon')
|
||||
def test_start_iscsi_target(self, mock_wait_iscsi, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
mock_execute.return_value = ('', '')
|
||||
result = self.agent_extension.start_iscsi_target(iqn=self.fake_iqn)
|
||||
|
||||
expected = [mock.call('tgtd', check_exit_code=[0]),
|
||||
mock.call('tgtadm', '--lld', 'iscsi', '--mode',
|
||||
'target', '--op', 'new', '--tid', '1',
|
||||
'--targetname', self.fake_iqn,
|
||||
check_exit_code=[0]),
|
||||
mock.call('tgtadm', '--lld', 'iscsi', '--mode',
|
||||
'logicalunit', '--op', 'new', '--tid', '1',
|
||||
'--lun', '1', '--backing-store',
|
||||
self.fake_dev, check_exit_code=[0]),
|
||||
mock.call('tgtadm', '--lld', 'iscsi', '--mode', 'target',
|
||||
'--op', 'bind', '--tid', '1',
|
||||
'--initiator-address', 'ALL',
|
||||
check_exit_code=[0])]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
mock_wait_iscsi.assert_called_once_with()
|
||||
self.assertEqual({'iscsi_target_iqn': self.fake_iqn},
|
||||
result.command_result)
|
||||
|
||||
def test_start_iscsi_target_fail_wait_daemon(self, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
mock_execute.return_value = ('', '')
|
||||
self.assertRaises(errors.ISCSIError,
|
||||
self.agent_extension.start_iscsi_target,
|
||||
iqn=self.fake_iqn)
|
||||
mock_execute.assert_called_once_with('tgtd', check_exit_code=[0])
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
|
||||
@mock.patch.object(iscsi, '_wait_for_iscsi_daemon')
|
||||
def test_start_iscsi_target_fail_command(self, mock_wait_iscsi,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
mock_execute.side_effect = [('', ''),
|
||||
processutils.ProcessExecutionError('blah')]
|
||||
self.assertRaises(errors.ISCSIError,
|
||||
self.agent_extension.start_iscsi_target,
|
||||
iqn=self.fake_iqn)
|
||||
|
||||
expected = [mock.call('tgtd', check_exit_code=[0]),
|
||||
mock.call('tgtadm', '--lld', 'iscsi', '--mode',
|
||||
'target', '--op', 'new', '--tid', '1',
|
||||
'--targetname', self.fake_iqn,
|
||||
check_exit_code=[0])]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
@ -22,6 +22,7 @@ ironic_python_agent.extensions =
|
||||
standby = ironic_python_agent.extensions.standby:StandbyExtension
|
||||
decom = ironic_python_agent.extensions.decom:DecomExtension
|
||||
flow = ironic_python_agent.extensions.flow:FlowExtension
|
||||
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||
|
||||
ironic_python_agent.hardware_managers =
|
||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||
|
Loading…
x
Reference in New Issue
Block a user