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:
Lucas Alvares Gomes 2015-02-10 16:30:21 +00:00
parent f1dec5c2e2
commit d3aa7c93aa
5 changed files with 198 additions and 2 deletions
Dockerfile
ironic_python_agent
errors.py
extensions
tests/extensions
setup.cfg

@ -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)

@ -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}

@ -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