[Kimchi-devel] [PATCH v3 4/5] storagepool: Support Creating iSCSI storagepool in model.py

Zhou Zheng Sheng zhshzhou at linux.vnet.ibm.com
Fri Dec 27 10:20:08 UTC 2013


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 at 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.
+                  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 at 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):
+    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]
+
+    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'])
+
+    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:
-- 
1.7.11.7




More information about the Kimchi-devel mailing list