On 2013年12月27日 18:20, Zhou Zheng Sheng wrote:
This patch implements creating iSCSI storagepool for libvirt.
Each LUN in iSCSI storagepool can be used as a volume, but iSCSI
storagepool does not provide ability to create volume. For now in kimchi
we create volume for each newly created VM. Next is to implement
attaching existing volume to a new VM.
This patch also adds validation for the iSCSI back-end. It tries to
login the target using the auth before actually creating the pool.
On RedHat family distributions, libiscsi comes with a Python binding,
but not on other distributions. So in this patch it invokes iscsiadm to
check iSCSI target.
As regard to iSCSI CHAP authentication, it's somewhat broken before
libvirt 1.1.1. It was reworked in libvirt commit eb0d79, and this patch
goes with libvirt 1.1.1, but it doesn't require libvirt in the spec
file. This is because libvirt 1.1.1 is not available on Ubuntu 12.04
LTS. The user can just ignore this feature.
How to test it manually
Prerequisite
An running iSCSI target on the network.
Create:
curl -u root -H 'Content-type: application/json' \
-H 'Accept: application/json' \
-d '{"source": {
"target": "iqn.YouriSCSITargetIQN.XXX",
"host": "10.0.0.X"},
"type": "iscsi",
"name": "iscsipool"}' \
http://127.0.0.1:8000/storagepools
Show Info:
curl -u root -H 'Accept: application/json' \
http://127.0.0.1:8000/storagepools/iscsipool
Activate:
curl -u root -H 'Content-type: application/json' \
-H 'Accept: application/json' \
-d ''
http://127.0.0.1:8000/storagepools/iscsipool/activate
Examine:
iscsiadm -m session
Deactivate:
curl -u root -H 'Content-type: application/json' \
-H 'Accept: application/json' \
-d ''
http://127.0.0.1:8000/storagepools/iscsipool/deactivate
Delete:
curl -u root -X DELETE -H 'Accept: application/json' \
http://127.0.0.1:8000/storagepools/iscsipool
v2 -> v3
1) Support CHAP authentication.
2) Validate iSCSI target before creating the pool.
3) Add XML generation unit test with "port" and "auth".
Signed-off-by: Zhou Zheng Sheng <zhshzhou(a)linux.vnet.ibm.com>
---
Makefile.am | 1 +
docs/API.md | 12 +++++--
src/kimchi/Makefile.am | 1 +
src/kimchi/iscsi.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++
src/kimchi/model.py | 88 ++++++++++++++++++++++++++++++++++++++++++++--
tests/test_storagepool.py | 63 +++++++++++++++++++++++++++++++++
6 files changed, 249 insertions(+), 5 deletions(-)
create mode 100644 src/kimchi/iscsi.py
diff --git a/Makefile.am b/Makefile.am
index 33059a7..b87f5d0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -43,6 +43,7 @@ PEP8_WHITELIST = \
src/kimchi/cachebust.py \
src/kimchi/config.py.in \
src/kimchi/disks.py \
+ src/kimchi/iscsi.py \
src/kimchi/root.py \
src/kimchi/server.py \
plugins/__init__.py \
diff --git a/docs/API.md b/docs/API.md
index a8b9ea0..863f6ad 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -182,17 +182,25 @@ Represents a snapshot of the Virtual Machine's primary
monitor.
* **POST**: Create a new Storage Pool
* name: The name of the Storage Pool.
* type: The type of the defined Storage Pool.
- Supported types: 'dir', 'kimchi-iso', 'netfs',
'logical'
+ Supported types: 'dir', 'kimchi-iso', 'netfs',
'logical', 'iscsi'
* path: The path of the defined Storage Pool.
For 'kimchi-iso' pool refers to targeted deep scan path.
Pool types: 'dir', 'kimchi-iso'.
* source: Dictionary containing source information of the pool.
* host: IP or hostname of server for a pool backed from a remote host.
- Pool types: 'netfs'.
+ Pool types: 'netfs', 'iscsi'.
* path: Export path on NFS server for NFS pool.
Pool types: 'netfs'.
* devices: Array of devices to be used in the Storage Pool
Pool types: 'logical'.
+ * target: Target IQN of an iSCSI pooVl.
Typo?
+ Pool types: 'iscsi'.
+ * port *(optional)*: Listening port of a remote storage server.
+ Pool types: 'iscsi'.
+ * auth *(optional)*: Storage back-end authentication information.
+ Pool types: 'iscsi'.
+ * username: Login username of the iSCSI target.
+ * password: Login password of the iSCSI target.
### Resource: Storage Pool
diff --git a/src/kimchi/Makefile.am b/src/kimchi/Makefile.am
index 47a3bd2..02b6fee 100644
--- a/src/kimchi/Makefile.am
+++ b/src/kimchi/Makefile.am
@@ -28,6 +28,7 @@ kimchi_PYTHON = \
distroloader.py \
exception.py \
__init__.py \
+ iscsi.py \
isoinfo.py \
netinfo.py \
network.py \
diff --git a/src/kimchi/iscsi.py b/src/kimchi/iscsi.py
new file mode 100644
index 0000000..51cffbb
--- /dev/null
+++ b/src/kimchi/iscsi.py
@@ -0,0 +1,89 @@
+#
+# Project Kimchi
+#
+# Copyright IBM, Corp. 2013
+#
+# Authors:
+# Zhou Zheng Sheng <zhshzhou(a)linux.vnet.ibm.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA
+
+import subprocess
+
+
+from kimchi.exception import OperationFailed
+
+
+class IscsiTarget(object):
What about change its name to IscsiCli?or sth like
initiator? Target
sounds like a server
+ def __init__(self, target, host, port=None, auth=None):
+ self.portal = host + ("" if port is None else ":%s" % port)
+ self.target = target
+ self.auth = auth
+ self.targetCmd = ['iscsiadm', '--mode', 'node',
'--targetname',
+ self.target, '--portal', self.portal]
The cmd
iscsiadm is at least not installed by default in my Ubuntu
13.04, I think we need to update the spec file.
+
+ def _update_db(self, Name, Value):
+ self._run_cmd(['--op=update', '--name', Name, '--value',
Value])
+
+ def _update_auth(self):
+ if self.auth is None:
+ items = (('node.session.auth.authmethod', 'None'),
+ ('node.session.auth.username', ''),
+ ('node.session.auth.password', ''))
+ else:
+ items = (('node.session.auth.authmethod', 'CHAP'),
+ ('node.session.auth.username',
self.auth['username']),
+ ('node.session.auth.password',
self.auth['password']))
+ for name, value in items:
+ self._update_db(name, value)
+
+ def _run_cmd(self, cmd):
+ iscsiadm = subprocess.Popen(
+ self.targetCmd + cmd,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = iscsiadm.communicate()
+ if iscsiadm.returncode != 0:
+ raise OperationFailed('Error executing iscsiadm: %s' % err)
+ return out
+
+ def _discover(self):
+ iscsiadm = subprocess.Popen(
+ ['iscsiadm', '--mode', 'discovery',
'--type', 'sendtargets',
+ '--portal', self.portal],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = iscsiadm.communicate()
+ if iscsiadm.returncode != 0:
+ raise OperationFailed('Error executing iscsiadm: %s' % err)
+ return out
+
+ def _run_op(self, op):
+ self._run_cmd(['--' + op])
+
+ def login(self):
+ self._discover()
+ self._update_auth()
+ self._run_op('login')
+
+ def logout(self):
+ self._run_op('logout')
+
+ def validate(self):
+ try:
+ self.login()
+ except OperationFailed:
+ return False
+
+ self.logout()
+ return True
diff --git a/src/kimchi/model.py b/src/kimchi/model.py
index b6213e9..39795c9 100644
--- a/src/kimchi/model.py
+++ b/src/kimchi/model.py
@@ -65,6 +65,7 @@ from kimchi.distroloader import DistroLoader
from kimchi.exception import InvalidOperation, InvalidParameter, MissingParameter
from kimchi.exception import NotFoundError, OperationFailed
from kimchi.featuretests import FeatureTests
+from kimchi.iscsi import IscsiTarget
from kimchi.networkxml import to_network_xml
from kimchi.objectstore import ObjectStore
from kimchi.scan import Scanner
@@ -995,7 +996,7 @@ class Model(object):
if params['type'] == 'kimchi-iso':
task_id = self._do_deep_scan(params)
poolDef = StoragePoolDef.create(params)
- poolDef.prepare()
+ poolDef.prepare(conn)
xml = poolDef.get_xml()
except KeyError, key:
raise MissingParameter(key)
@@ -1444,7 +1445,7 @@ class StoragePoolDef(object):
def __init__(self, poolArgs):
self.poolArgs = poolArgs
- def prepare(self):
+ def prepare(self, conn):
''' Validate pool arguments and perform preparations. Operation
which
would cause side effect should be put here. Subclasses can optionally
override this method, or it always succeeds by default. '''
@@ -1489,7 +1490,7 @@ class NetfsPoolDef(StoragePoolDef):
super(NetfsPoolDef, self).__init__(poolArgs)
self.path = '/var/lib/kimchi/nfs_mount/' +
self.poolArgs['name']
- def prepare(self):
+ def prepare(self, conn):
# TODO Verify the NFS export can be actually mounted.
pass
@@ -1548,6 +1549,87 @@ class LogicalPoolDef(StoragePoolDef):
return xml
+class IscsiPoolDef(StoragePoolDef):
+ poolType = 'iscsi'
+
+ def prepare(self, conn):
+ source = self.poolArgs['source']
+ if not IscsiTarget(**source).validate():
+ raise OperationFailed("Can not login to iSCSI host %s target %s"
%
+ (source['host'], source['target']))
+ self._prepare_auth(conn)
+
+ def _prepare_auth(self, conn):
+ try:
+ auth = self.poolArgs['source']['auth']
+ except KeyError:
+ return
+
+ try:
+ virSecret = conn.secretLookupByUsage(
+ libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name'])
+ except libvirt.libvirtError:
+ xml = '''
+ <secret ephemeral='no' private='yes'>
+ <description>Secret for iSCSI storage pool
{name}</description>
+ <auth type='chap' username='{username}'/>
+ <usage type='iscsi'>
+ <target>{name}</target>
+ </usage>
+ </secret>'''.format(name=self.poolArgs['name'],
+ username=auth['username'])
+ virSecret = conn.secretDefineXML(xml)
+
+ virSecret.setValue(auth['password'])
+
+ def _format_port(self, poolArgs):
+ try:
+ port = poolArgs['source']['port']
+ except KeyError:
+ return ""
+ return "port='%s'" % port
+
+ def _format_auth(self, poolArgs):
+ try:
+ auth = poolArgs['source']['auth']
+ except KeyError:
+ return ""
+
+ return '''
+ <auth type='chap' username='{username}'>
+ <secret type='iscsi' usage='{name}'/>
+ </auth>'''.format(name=poolArgs['name'],
username=auth['username'])
I think these are suitable usecase for
@property.
+
+ def _get_xml_imp(self, poolArgs):
+ # Required parameters
+ # name:
+ # type:
+ # source[host]:
+ # source[target]:
+ #
+ # Optional parameters
+ # source[port]:
+
+ poolArgs['source'].update({'port': self._format_port(poolArgs),
+ 'auth': self._format_auth(poolArgs)})
+ poolArgs['path'] = '/dev/disk/by-id'
+
+ xml = """
+ <pool type='iscsi'>
+ <name>{name}</name>
+ <source>
+ <host name='{source[host]}' {source[port]}/>
+ <device path='{source[target]}'/>
+ {source[auth]}
+ </source>
+ <target>
+ <path>{path}</path>
+ </target>
+ </pool>
+ """.format(**poolArgs)
+ return xml
+
+
def _get_volume_xml(**kwargs):
# Required parameters
# name:
diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py
index beecdb6..c3f08ad 100644
--- a/tests/test_storagepool.py
+++ b/tests/test_storagepool.py
@@ -78,6 +78,69 @@ class storagepoolTests(unittest.TestCase):
<path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path>
</target>
</pool>
+ """},
+ {'def':
+ {'type': 'iscsi',
+ 'name': 'unitTestISCSIPool',
+ 'source': {
+ 'host': '127.0.0.1',
+ 'target':
'iqn.2003-01.org.linux-iscsi.localhost'}},
+ 'xml':
+ """
+ <pool type='iscsi'>
+ <name>unitTestISCSIPool</name>
+ <source>
+ <host name='127.0.0.1' />
+ <device path='iqn.2003-01.org.linux-iscsi.localhost'/>
+ </source>
+ <target>
+ <path>/dev/disk/by-id</path>
+ </target>
+ </pool>
+ """},
+ {'def':
+ {'type': 'iscsi',
+ 'name': 'unitTestISCSIPoolPort',
+ 'source': {
+ 'host': '127.0.0.1',
+ 'port': 3266,
+ 'target':
'iqn.2003-01.org.linux-iscsi.localhost'}},
+ 'xml':
+ """
+ <pool type='iscsi'>
+ <name>unitTestISCSIPoolPort</name>
+ <source>
+ <host name='127.0.0.1' port='3266' />
+ <device path='iqn.2003-01.org.linux-iscsi.localhost'/>
+ </source>
+ <target>
+ <path>/dev/disk/by-id</path>
+ </target>
+ </pool>
+ """},
+ {'def':
+ {'type': 'iscsi',
+ 'name': 'unitTestISCSIPoolAuth',
+ 'source': {
+ 'host': '127.0.0.1',
+ 'target': 'iqn.2003-01.org.linux-iscsi.localhost',
+ 'auth': {'username': 'testUser',
+ 'password':
'ActuallyNotUsedInPoolXML'}}},
+ 'xml':
+ """
+ <pool type='iscsi'>
+ <name>unitTestISCSIPoolAuth</name>
+ <source>
+ <host name='127.0.0.1' />
+ <device path='iqn.2003-01.org.linux-iscsi.localhost'/>
+ <auth type='chap' username='testUser'>
+ <secret type='iscsi'
usage='unitTestISCSIPoolAuth'/>
+ </auth>
+ </source>
+ <target>
+ <path>/dev/disk/by-id</path>
+ </target>
+ </pool>
"""}]
for poolDef in poolDefs: