]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
FibreChannel drivers for NetApp Data ONTAP storage controllers
authorClinton Knight <cknight@netapp.com>
Thu, 9 Oct 2014 14:56:25 +0000 (10:56 -0400)
committerClinton Knight <cknight@netapp.com>
Fri, 12 Dec 2014 17:08:49 +0000 (17:08 +0000)
This patch adds FibreChannel support to NetApp's Cinder drivers
for Data ONTAP (7-mode and Cluster-mode).  The drivers make full
use of Cinder's FibreChannel zone manager.

Implements blueprint add-fibre-channel-support-to-netapp-drivers
Change-Id: Ifbda275e4a60dda144a169ef00a4ea5e548dfa03

21 files changed:
cinder/tests/volume/drivers/netapp/dataontap/__init__.py [new file with mode: 0644]
cinder/tests/volume/drivers/netapp/dataontap/client/test_client_7mode.py
cinder/tests/volume/drivers/netapp/dataontap/client/test_client_base.py
cinder/tests/volume/drivers/netapp/dataontap/client/test_client_cmode.py
cinder/tests/volume/drivers/netapp/dataontap/fakes.py [new file with mode: 0644]
cinder/tests/volume/drivers/netapp/dataontap/test_block_7mode.py
cinder/tests/volume/drivers/netapp/dataontap/test_block_base.py
cinder/tests/volume/drivers/netapp/dataontap/test_block_cmode.py
cinder/tests/volume/drivers/netapp/eseries/__init__.py [new file with mode: 0644]
cinder/tests/volume/drivers/netapp/fakes.py [new file with mode: 0644]
cinder/volume/drivers/netapp/common.py
cinder/volume/drivers/netapp/dataontap/block_7mode.py
cinder/volume/drivers/netapp/dataontap/block_base.py
cinder/volume/drivers/netapp/dataontap/block_cmode.py
cinder/volume/drivers/netapp/dataontap/client/client_7mode.py
cinder/volume/drivers/netapp/dataontap/client/client_base.py
cinder/volume/drivers/netapp/dataontap/client/client_cmode.py
cinder/volume/drivers/netapp/dataontap/fc_7mode.py [new file with mode: 0644]
cinder/volume/drivers/netapp/dataontap/fc_cmode.py [new file with mode: 0644]
cinder/volume/drivers/netapp/options.py
cinder/volume/drivers/netapp/utils.py

diff --git a/cinder/tests/volume/drivers/netapp/dataontap/__init__.py b/cinder/tests/volume/drivers/netapp/dataontap/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index bb1791be3eaf0cecef4d5ebd0d7394934055c162..01ace7447db570e180be04ee1ca391c346937a84 100644 (file)
@@ -20,9 +20,10 @@ import mock
 import six
 
 from cinder import test
+from cinder.tests.volume.drivers.netapp.dataontap import fakes as fake
 from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
 from cinder.volume.drivers.netapp.dataontap.client import client_7mode
-
+from cinder.volume.drivers.netapp.utils import hashabledict
 
 CONNECTION_INFO = {'hostname': 'hostname',
                    'transport_type': 'https',
@@ -51,7 +52,7 @@ class NetApp7modeClientTestCase(test.TestCase):
     def tearDown(self):
         super(NetApp7modeClientTestCase, self).tearDown()
 
-    def test_get_target_details_no_targets(self):
+    def test_get_iscsi_target_details_no_targets(self):
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
                            <iscsi-portal-list-entries>
@@ -59,11 +60,11 @@ class NetApp7modeClientTestCase(test.TestCase):
                          </results>"""))
         self.connection.invoke_successfully.return_value = response
 
-        target_list = self.client.get_target_details()
+        target_list = self.client.get_iscsi_target_details()
 
         self.assertEqual([], target_list)
 
-    def test_get_target_details(self):
+    def test_get_iscsi_target_details(self):
         expected_target = {
             "address": "127.0.0.1",
             "port": "1337",
@@ -81,7 +82,7 @@ class NetApp7modeClientTestCase(test.TestCase):
                           </results>""" % expected_target))
         self.connection.invoke_successfully.return_value = response
 
-        target_list = self.client.get_target_details()
+        target_list = self.client.get_iscsi_target_details()
 
         self.assertEqual([expected_target], target_list)
 
@@ -121,8 +122,9 @@ class NetApp7modeClientTestCase(test.TestCase):
 
         self.assertEqual(2, len(luns))
 
-    def test_get_igroup_by_initiator_none_found(self):
-        initiator = 'initiator'
+    def test_get_igroup_by_initiators_none_found(self):
+        initiators = fake.FC_FORMATTED_INITIATORS[0]
+
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
                            <initiator-groups>
@@ -130,36 +132,89 @@ class NetApp7modeClientTestCase(test.TestCase):
                          </results>"""))
         self.connection.invoke_successfully.return_value = response
 
-        igroup = self.client.get_igroup_by_initiator(initiator)
+        igroup = self.client.get_igroup_by_initiators(initiators)
 
         self.assertEqual([], igroup)
 
-    def test_get_igroup_by_initiator(self):
-        initiator = 'initiator'
-        expected_igroup = {
-            "initiator-group-os-type": None,
-            "initiator-group-type": "1337",
-            "initiator-group-name": "vserver",
-        }
+    def test_get_igroup_by_initiators(self):
+        initiators = [fake.FC_FORMATTED_INITIATORS[0]]
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
-                           <initiator-groups>
-                             <initiator-group-info>
-                               <initiators>
-                                 <initiator-info>
-                                   <initiator-name>initiator</initiator-name>
-                                 </initiator-info>
-                               </initiators>
-    <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
-    <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
-                             </initiator-group-info>
-                           </initiator-groups>
-                         </results>""" % expected_igroup))
+    <initiator-groups>
+      <initiator-group-info>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-uuid>1477ee47-0e1f-4b35-a82c-dcca0b76fc44
+        </initiator-group-uuid>
+        <initiator-group-os-type>linux</initiator-group-os-type>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-throttle-borrow>false
+        </initiator-group-throttle-borrow>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-report-scsi-name-enabled>true
+        </initiator-group-report-scsi-name-enabled>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiators>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c3</initiator-name>
+          </initiator-info>
+        </initiators>
+      </initiator-group-info>
+    </initiator-groups>
+  </results>""" % fake.IGROUP1))
+        self.connection.invoke_successfully.return_value = response
+
+        igroups = self.client.get_igroup_by_initiators(initiators)
+
+        # make these lists of dicts comparable using hashable dictionaries
+        igroups = set([hashabledict(igroup) for igroup in igroups])
+        expected = set([hashabledict(fake.IGROUP1)])
+
+        self.assertSetEqual(igroups, expected)
+
+    def test_get_igroup_by_initiators_multiple(self):
+        initiators = fake.FC_FORMATTED_INITIATORS
+        response = netapp_api.NaElement(
+            etree.XML("""<results status="passed">
+    <initiator-groups>
+      <initiator-group-info>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-uuid>1477ee47-0e1f-4b35-a82c-dcca0b76fc44
+        </initiator-group-uuid>
+        <initiator-group-os-type>linux</initiator-group-os-type>
+        <initiators>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c3</initiator-name>
+          </initiator-info>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c2</initiator-name>
+          </initiator-info>
+        </initiators>
+      </initiator-group-info>
+      <initiator-group-info>
+        <initiator-group-name>openstack-igroup2</initiator-group-name>
+        <initiator-group-type>fcp</initiator-group-type>
+        <initiator-group-uuid>1477ee47-0e1f-4b35-a82c-dcca0b76fc44
+        </initiator-group-uuid>
+        <initiator-group-os-type>linux</initiator-group-os-type>
+        <initiators>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c2</initiator-name>
+          </initiator-info>
+        </initiators>
+      </initiator-group-info>    </initiator-groups>
+  </results>""" % fake.IGROUP1))
         self.connection.invoke_successfully.return_value = response
 
-        igroup = self.client.get_igroup_by_initiator(initiator)
+        igroups = self.client.get_igroup_by_initiators(initiators)
 
-        self.assertEqual([expected_igroup], igroup)
+        # make these lists of dicts comparable using hashable dictionaries
+        igroups = set([hashabledict(igroup) for igroup in igroups])
+        expected = set([hashabledict(fake.IGROUP1)])
+
+        self.assertSetEqual(igroups, expected)
 
     def test_clone_lun(self):
         fake_clone_start = netapp_api.NaElement(
@@ -561,3 +616,28 @@ class NetApp7modeClientTestCase(test.TestCase):
         actual_request = _args[0]
         self.assertEqual('net-ifconfig-get', actual_request.get_name())
         self.assertEqual(expected_response, actual_response)
+
+    def test_get_fc_target_wwpns(self):
+        wwpn1 = '50:0a:09:81:90:fe:eb:a5'
+        wwpn2 = '50:0a:09:82:90:fe:eb:a5'
+        response = netapp_api.NaElement(
+            etree.XML("""
+  <results status="passed">
+    <fcp-port-names>
+      <fcp-port-name-info>
+        <port-name>%(wwpn1)s</port-name>
+        <is-used>true</is-used>
+        <fcp-adapter>1a</fcp-adapter>
+      </fcp-port-name-info>
+      <fcp-port-name-info>
+        <port-name>%(wwpn2)s</port-name>
+        <is-used>true</is-used>
+        <fcp-adapter>1b</fcp-adapter>
+      </fcp-port-name-info>
+    </fcp-port-names>
+  </results>""" % {'wwpn1': wwpn1, 'wwpn2': wwpn2}))
+        self.connection.invoke_successfully.return_value = response
+
+        wwpns = self.client.get_fc_target_wwpns()
+
+        self.assertSetEqual(set(wwpns), set([wwpn1, wwpn2]))
index b84a53b230c69267617c896d26d5414b8f815403..e0f73a5378e9b42d1c46d2f3b855147ef4e2cf72 100644 (file)
@@ -19,6 +19,7 @@ import mock
 import six
 
 from cinder import test
+import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake
 from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
 from cinder.volume.drivers.netapp.dataontap.client import client_base
 
@@ -61,7 +62,9 @@ class NetAppBaseClientTestCase(test.TestCase):
     def test_get_ontapi_version_cached(self):
 
         self.connection.get_api_version.return_value = (1, 20)
+
         major, minor = self.client.get_ontapi_version()
+
         self.assertEqual(1, self.connection.get_api_version.call_count)
         self.assertEqual(1, major)
         self.assertEqual(20, minor)
@@ -69,6 +72,7 @@ class NetAppBaseClientTestCase(test.TestCase):
     def test_check_is_naelement(self):
 
         element = netapp_api.NaElement('name')
+
         self.assertIsNone(self.client.check_is_naelement(element))
         self.assertRaises(ValueError, self.client.check_is_naelement, None)
 
@@ -366,6 +370,7 @@ class NetAppBaseClientTestCase(test.TestCase):
         self.connection.invoke_successfully.return_value = mock_response
 
         geometry = self.client.get_lun_geometry(path)
+
         self.assertEqual(expected_keys, set(geometry.keys()))
 
     def test_get_lun_geometry_with_api_error(self):
@@ -380,6 +385,7 @@ class NetAppBaseClientTestCase(test.TestCase):
         fake_response = netapp_api.NaElement('volume')
         fake_response.add_node_with_children('options', test='blah')
         self.connection.invoke_successfully.return_value = fake_response
+
         options = self.client.get_volume_options('volume')
 
         self.assertEqual(1, len(options))
@@ -387,6 +393,7 @@ class NetAppBaseClientTestCase(test.TestCase):
     def test_get_volume_options_with_no_options(self):
         fake_response = netapp_api.NaElement('options')
         self.connection.invoke_successfully.return_value = fake_response
+
         options = self.client.get_volume_options('volume')
 
         self.assertEqual([], options)
@@ -396,7 +403,66 @@ class NetAppBaseClientTestCase(test.TestCase):
         new_path = '/vol/%s/%s' % (self.fake_volume, self.fake_lun)
         fake_response = netapp_api.NaElement('options')
         self.connection.invoke_successfully.return_value = fake_response
+
         self.client.move_lun(path, new_path)
 
         self.connection.invoke_successfully.assert_called_once_with(
             mock.ANY, True)
+
+    def test_get_igroup_by_initiators(self):
+        self.assertRaises(NotImplementedError,
+                          self.client.get_igroup_by_initiators,
+                          fake.FC_FORMATTED_INITIATORS)
+
+    def test_get_fc_target_wwpns(self):
+        self.assertRaises(NotImplementedError,
+                          self.client.get_fc_target_wwpns)
+
+    def test_has_luns_mapped_to_initiator(self):
+        initiator = fake.FC_FORMATTED_INITIATORS[0]
+        version_response = netapp_api.NaElement(
+            etree.XML("""
+  <results status="passed">
+    <lun-maps>
+      <lun-map-info>
+        <path>/vol/cinder1/volume-9be956b3-9854-4a5c-a7f5-13a16da52c9c</path>
+        <initiator-group>openstack-4b57a80b-ebca-4d27-bd63-48ac5408d08b
+        </initiator-group>
+        <lun-id>0</lun-id>
+      </lun-map-info>
+      <lun-map-info>
+        <path>/vol/cinder1/volume-ac90433c-a560-41b3-9357-7f3f80071eb5</path>
+        <initiator-group>openstack-4b57a80b-ebca-4d27-bd63-48ac5408d08b
+        </initiator-group>
+        <lun-id>1</lun-id>
+      </lun-map-info>
+    </lun-maps>
+  </results>"""))
+
+        self.connection.invoke_successfully.return_value = version_response
+
+        self.assertTrue(self.client._has_luns_mapped_to_initiator(initiator))
+
+    def test_has_luns_mapped_to_initiator_not_mapped(self):
+        initiator = fake.FC_FORMATTED_INITIATORS[0]
+        version_response = netapp_api.NaElement(
+            etree.XML("""
+  <results status="passed">
+    <lun-maps />
+  </results>"""))
+        self.connection.invoke_successfully.return_value = version_response
+        self.assertFalse(self.client._has_luns_mapped_to_initiator(initiator))
+
+    @mock.patch.object(client_base.Client, '_has_luns_mapped_to_initiator')
+    def test_has_luns_mapped_to_initiators(self,
+                                           mock_has_luns_mapped_to_initiator):
+        initiators = fake.FC_FORMATTED_INITIATORS
+        mock_has_luns_mapped_to_initiator.return_value = True
+        self.assertTrue(self.client.has_luns_mapped_to_initiators(initiators))
+
+    @mock.patch.object(client_base.Client, '_has_luns_mapped_to_initiator')
+    def test_has_luns_mapped_to_initiators_not_mapped(
+            self, mock_has_luns_mapped_to_initiator):
+        initiators = fake.FC_FORMATTED_INITIATORS
+        mock_has_luns_mapped_to_initiator.return_value = False
+        self.assertFalse(self.client.has_luns_mapped_to_initiators(initiators))
index 40f74a888463fecd2da7499810451670397ca623..b9ef9ae9097946b55e6f80a6d4b23ae560198774 100644 (file)
@@ -23,6 +23,7 @@ from cinder import exception
 from cinder import test
 from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
 from cinder.volume.drivers.netapp.dataontap.client import client_cmode
+from cinder.volume.drivers.netapp.utils import hashabledict
 
 
 CONNECTION_INFO = {'hostname': 'hostname',
@@ -52,18 +53,18 @@ class NetAppCmodeClientTestCase(test.TestCase):
     def tearDown(self):
         super(NetAppCmodeClientTestCase, self).tearDown()
 
-    def test_get_target_details_no_targets(self):
+    def test_get_iscsi_target_details_no_targets(self):
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
                             <num-records>1</num-records>
                             <attributes-list></attributes-list>
                           </results>"""))
         self.connection.invoke_successfully.return_value = response
-        target_list = self.client.get_target_details()
+        target_list = self.client.get_iscsi_target_details()
 
         self.assertEqual([], target_list)
 
-    def test_get_target_details(self):
+    def test_get_iscsi_target_details(self):
         expected_target = {
             "address": "127.0.0.1",
             "port": "1337",
@@ -84,7 +85,7 @@ class NetAppCmodeClientTestCase(test.TestCase):
                           </results>""" % expected_target))
         self.connection.invoke_successfully.return_value = response
 
-        target_list = self.client.get_target_details()
+        target_list = self.client.get_iscsi_target_details()
 
         self.assertEqual([expected_target], target_list)
 
@@ -241,67 +242,169 @@ class NetAppCmodeClientTestCase(test.TestCase):
                           </results>"""))
         self.connection.invoke_successfully.return_value = response
 
-        igroup = self.client.get_igroup_by_initiator(initiator)
+        igroup = self.client.get_igroup_by_initiators([initiator])
 
         self.assertEqual([], igroup)
 
-    def test_get_igroup_by_initiator(self):
-        initiator = 'initiator'
+    def test_get_igroup_by_initiators(self):
+        initiators = ['11:22:33:44:55:66:77:88']
         expected_igroup = {
-            "initiator-group-os-type": None,
-            "initiator-group-type": "1337",
-            "initiator-group-name": "vserver",
+            'initiator-group-os-type': 'default',
+            'initiator-group-type': 'fcp',
+            'initiator-group-name': 'openstack-igroup1',
         }
+
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
-                            <num-records>1</num-records>
-                            <attributes-list>
-                              <initiator-group-info>
-    <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
-    <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
-                              </initiator-group-info>
-                            </attributes-list>
-                          </results>""" % expected_igroup))
+    <attributes-list>
+      <initiator-group-info>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-os-type>default</initiator-group-os-type>
+        <initiator-group-throttle-borrow>false</initiator-group-throttle-borrow>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiator-group-uuid>f8aa707a-57fa-11e4-ad08-123478563412
+        </initiator-group-uuid>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiators>
+          <initiator-info>
+            <initiator-name>11:22:33:44:55:66:77:88</initiator-name>
+          </initiator-info>
+        </initiators>
+        <vserver>cinder-iscsi</vserver>
+      </initiator-group-info>
+    </attributes-list>
+    <num-records>1</num-records>
+  </results>""" % expected_igroup))
         self.connection.invoke_successfully.return_value = response
 
-        igroup = self.client.get_igroup_by_initiator(initiator)
+        igroups = self.client.get_igroup_by_initiators(initiators)
 
-        self.assertEqual([expected_igroup], igroup)
+        # make these lists of dicts comparable using hashable dictionaries
+        igroups = set([hashabledict(igroup) for igroup in igroups])
+        expected = set([hashabledict(expected_igroup)])
 
-    def test_get_igroup_by_initiator_multiple_pages(self):
-        initiator = 'initiator'
+        self.assertSetEqual(igroups, expected)
+
+    def test_get_igroup_by_initiators_multiple(self):
+        initiators = ['11:22:33:44:55:66:77:88', '88:77:66:55:44:33:22:11']
         expected_igroup = {
-            "initiator-group-os-type": None,
-            "initiator-group-type": "1337",
-            "initiator-group-name": "vserver",
+            'initiator-group-os-type': 'default',
+            'initiator-group-type': 'fcp',
+            'initiator-group-name': 'openstack-igroup1',
         }
+
         response = netapp_api.NaElement(
             etree.XML("""<results status="passed">
-                            <num-records>1</num-records>
-                            <attributes-list>
-                              <initiator-group-info>
-    <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
-    <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
-                              </initiator-group-info>
-                            </attributes-list>
-                            <next-tag>blah</next-tag>
-                          </results>""" % expected_igroup))
+    <attributes-list>
+      <initiator-group-info>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-os-type>default</initiator-group-os-type>
+        <initiator-group-throttle-borrow>false</initiator-group-throttle-borrow>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiator-group-uuid>f8aa707a-57fa-11e4-ad08-123478563412
+        </initiator-group-uuid>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiators>
+          <initiator-info>
+            <initiator-name>11:22:33:44:55:66:77:88</initiator-name>
+          </initiator-info>
+          <initiator-info>
+            <initiator-name>88:77:66:55:44:33:22:11</initiator-name>
+          </initiator-info>
+        </initiators>
+        <vserver>cinder-iscsi</vserver>
+      </initiator-group-info>
+    </attributes-list>
+    <num-records>1</num-records>
+  </results>""" % expected_igroup))
+        self.connection.invoke_successfully.return_value = response
+
+        igroups = self.client.get_igroup_by_initiators(initiators)
+
+        # make these lists of dicts comparable using hashable dictionaries
+        igroups = set([hashabledict(igroup) for igroup in igroups])
+        expected = set([hashabledict(expected_igroup)])
+
+        self.assertSetEqual(igroups, expected)
+
+    def test_get_igroup_by_initiators_multiple_pages(self):
+        initiator = '11:22:33:44:55:66:77:88'
+        expected_igroup1 = {
+            'initiator-group-os-type': 'default',
+            'initiator-group-type': 'fcp',
+            'initiator-group-name': 'openstack-igroup1',
+        }
+        expected_igroup2 = {
+            'initiator-group-os-type': 'default',
+            'initiator-group-type': 'fcp',
+            'initiator-group-name': 'openstack-igroup2',
+        }
+        response_1 = netapp_api.NaElement(
+            etree.XML("""<results status="passed">
+    <attributes-list>
+      <initiator-group-info>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-os-type>default</initiator-group-os-type>
+        <initiator-group-throttle-borrow>false</initiator-group-throttle-borrow>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiator-group-uuid>f8aa707a-57fa-11e4-ad08-123478563412
+        </initiator-group-uuid>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiators>
+          <initiator-info>
+            <initiator-name>11:22:33:44:55:66:77:88</initiator-name>
+          </initiator-info>
+        </initiators>
+        <vserver>cinder-iscsi</vserver>
+      </initiator-group-info>
+    </attributes-list>
+    <next-tag>12345</next-tag>
+    <num-records>1</num-records>
+  </results>""" % expected_igroup1))
         response_2 = netapp_api.NaElement(
             etree.XML("""<results status="passed">
-                            <num-records>1</num-records>
-                            <attributes-list>
-                              <initiator-group-info>
-    <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
-    <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
-                              </initiator-group-info>
-                            </attributes-list>
-                          </results>""" % expected_igroup))
-        self.connection.invoke_successfully.side_effect = [response,
+    <attributes-list>
+      <initiator-group-info>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-os-type>default</initiator-group-os-type>
+        <initiator-group-throttle-borrow>false</initiator-group-throttle-borrow>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiator-group-uuid>f8aa707a-57fa-11e4-ad08-123478563412
+        </initiator-group-uuid>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiators>
+          <initiator-info>
+            <initiator-name>11:22:33:44:55:66:77:88</initiator-name>
+          </initiator-info>
+        </initiators>
+        <vserver>cinder-iscsi</vserver>
+      </initiator-group-info>
+    </attributes-list>
+    <num-records>1</num-records>
+  </results>""" % expected_igroup2))
+        self.connection.invoke_successfully.side_effect = [response_1,
                                                            response_2]
 
-        igroup = self.client.get_igroup_by_initiator(initiator)
+        igroups = self.client.get_igroup_by_initiators([initiator])
+
+        # make these lists of dicts comparable using hashable dictionaries
+        igroups = set([hashabledict(igroup) for igroup in igroups])
+        expected = set([hashabledict(expected_igroup1),
+                        hashabledict(expected_igroup2)])
 
-        self.assertEqual([expected_igroup, expected_igroup], igroup)
+        self.assertSetEqual(igroups, expected)
 
     def test_clone_lun(self):
         self.client.clone_lun('volume', 'fakeLUN', 'newFakeLUN')
diff --git a/cinder/tests/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/volume/drivers/netapp/dataontap/fakes.py
new file mode 100644 (file)
index 0000000..7ff493e
--- /dev/null
@@ -0,0 +1,75 @@
+# Copyright (c) - 2014, Clinton Knight.  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.
+
+
+VOLUME = 'f10d1a84-9b7b-427e-8fec-63c48b509a56'
+LUN = 'ee6b4cc7-477b-4016-aa0c-7127b4e3af86'
+SIZE = '1024'
+METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'}
+
+UUID1 = '12345678-1234-5678-1234-567812345678'
+LUN1 = '/vol/vol0/lun1'
+IGROUP1_NAME = 'openstack-igroup1'
+VSERVER1_NAME = 'openstack-vserver'
+
+FC_VOLUME = {'name': 'fake_volume'}
+
+FC_INITIATORS = ['21000024ff406cc3', '21000024ff406cc2']
+FC_FORMATTED_INITIATORS = ['21:00:00:24:ff:40:6c:c3',
+                           '21:00:00:24:ff:40:6c:c2']
+
+FC_TARGET_WWPNS = ['500a098280feeba5', '500a098290feeba5',
+                   '500a098190feeba5', '500a098180feeba5']
+
+FC_FORMATTED_TARGET_WWPNS = ['50:0a:09:82:80:fe:eb:a5',
+                             '50:0a:09:82:90:fe:eb:a5',
+                             '50:0a:09:81:90:fe:eb:a5',
+                             '50:0a:09:81:80:fe:eb:a5']
+
+FC_CONNECTOR = {'ip': '1.1.1.1',
+                'host': 'fake_host',
+                'wwnns': ['20000024ff406cc3', '20000024ff406cc2'],
+                'wwpns': ['21000024ff406cc3', '21000024ff406cc2']}
+
+FC_I_T_MAP = {'21000024ff406cc3': ['500a098280feeba5', '500a098290feeba5'],
+              '21000024ff406cc2': ['500a098190feeba5', '500a098180feeba5']}
+
+FC_I_T_MAP_COMPLETE = {'21000024ff406cc3': FC_TARGET_WWPNS,
+                       '21000024ff406cc2': FC_TARGET_WWPNS}
+
+FC_FABRIC_MAP = {'fabricB':
+                 {'target_port_wwn_list':
+                  ['500a098190feeba5', '500a098180feeba5'],
+                  'initiator_port_wwn_list': ['21000024ff406cc2']},
+                 'fabricA':
+                 {'target_port_wwn_list':
+                  ['500a098290feeba5', '500a098280feeba5'],
+                  'initiator_port_wwn_list': ['21000024ff406cc3']}}
+
+FC_TARGET_INFO = {'driver_volume_type': 'fibre_channel',
+                  'data': {'target_lun': '1',
+                           'initiator_target_map': FC_I_T_MAP,
+                           'access_mode': 'rw',
+                           'target_wwn': FC_TARGET_WWPNS,
+                           'target_discovered': True}}
+
+FC_TARGET_INFO_EMPTY = {'driver_volume_type': 'fibre_channel', 'data': {}}
+
+FC_TARGET_INFO_UNMAP = {'driver_volume_type': 'fibre_channel',
+                        'data': {'target_wwn': FC_TARGET_WWPNS,
+                                 'initiator_target_map': FC_I_T_MAP}}
+
+IGROUP1 = {'initiator-group-os-type': 'linux',
+           'initiator-group-type': 'fcp',
+           'initiator-group-name': IGROUP1_NAME}
index ef5baf9206304a05210f72aa03306ce68a7a2999..aee2a3b5d5705fee996b22b81748c69057ed200d 100644 (file)
@@ -1,6 +1,5 @@
 # Copyright (c) 2014 Alex Meade.  All rights reserved.
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
-# 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
 Mock unit tests for the NetApp block storage 7-mode library
 """
 
-import uuid
 
+from lxml import etree
 import mock
-import six
 
+from cinder import exception
 from cinder import test
+import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake
+import cinder.tests.volume.drivers.netapp.fakes as na_fakes
 from cinder.volume.drivers.netapp.dataontap import block_7mode
+from cinder.volume.drivers.netapp.dataontap.block_7mode import \
+    NetAppBlockStorage7modeLibrary as block_lib_7mode
+from cinder.volume.drivers.netapp.dataontap.block_base import \
+    NetAppBlockStorageLibrary as block_lib
 from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
-
-FAKE_VOLUME = six.text_type(uuid.uuid4())
-FAKE_LUN = six.text_type(uuid.uuid4())
-FAKE_SIZE = '1024'
-FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'}
+from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError
+from cinder.volume.drivers.netapp.dataontap.client import client_base
 
 
 class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
@@ -38,17 +40,231 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
     def setUp(self):
         super(NetAppBlockStorage7modeLibraryTestCase, self).setUp()
 
-        kwargs = {'configuration': mock.Mock()}
-        self.library = block_7mode.NetAppBlockStorage7modeLibrary('driver',
-                                                                  'protocol',
-                                                                  **kwargs)
+        kwargs = {'configuration': self.get_config_7mode()}
+        self.library = block_lib_7mode('driver', 'protocol', **kwargs)
 
         self.library.zapi_client = mock.Mock()
+        self.zapi_client = self.library.zapi_client
         self.library.vfiler = mock.Mock()
 
     def tearDown(self):
         super(NetAppBlockStorage7modeLibraryTestCase, self).tearDown()
 
+    def get_config_7mode(self):
+        config = na_fakes.create_configuration_7mode()
+        config.netapp_storage_protocol = 'iscsi'
+        config.netapp_login = 'admin'
+        config.netapp_password = 'pass'
+        config.netapp_server_hostname = '127.0.0.1'
+        config.netapp_transport_type = 'http'
+        config.netapp_server_port = '80'
+        return config
+
+    @mock.patch.object(client_base.Client, 'get_ontapi_version',
+                       mock.MagicMock(return_value=(1, 20)))
+    @mock.patch.object(block_lib_7mode, '_get_root_volume_name')
+    @mock.patch.object(block_lib_7mode, '_do_partner_setup')
+    @mock.patch.object(block_lib, 'do_setup')
+    def test_do_setup(self, super_do_setup, mock_do_partner_setup,
+                      mock_get_root_volume_name):
+        mock_get_root_volume_name.return_value = 'vol0'
+        context = mock.Mock()
+
+        self.library.do_setup(context)
+
+        super_do_setup.assert_called_once_with(context)
+        mock_do_partner_setup.assert_called_once_with()
+        mock_get_root_volume_name.assert_called_once_with()
+
+    @mock.patch.object(client_base.Client, 'get_ontapi_version',
+                       mock.MagicMock(return_value=(1, 20)))
+    def test_do_partner_setup(self):
+        self.library.configuration.netapp_partner_backend_name = 'partner'
+
+        self.library._do_partner_setup()
+
+        self.assertIsNotNone(self.library.partner_zapi_client)
+
+    @mock.patch.object(client_base.Client, 'get_ontapi_version',
+                       mock.MagicMock(return_value=(1, 20)))
+    def test_do_partner_setup_no_partner(self):
+
+        self.library._do_partner_setup()
+
+        self.assertFalse(hasattr(self.library, 'partner_zapi_client'))
+
+    @mock.patch.object(block_lib, 'check_for_setup_error')
+    def test_check_for_setup_error(self, super_check_for_setup_error):
+        self.zapi_client.get_ontapi_version.return_value = (1, 9)
+
+        self.library.check_for_setup_error()
+
+        super_check_for_setup_error.assert_called_once_with()
+
+    def test_check_for_setup_error_too_old(self):
+        self.zapi_client.get_ontapi_version.return_value = (1, 8)
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.library.check_for_setup_error)
+
+    def test_find_mapped_lun_igroup(self):
+        response = netapp_api.NaElement(etree.XML("""
+<results status="passed">
+    <initiator-groups>
+      <initiator-group-info>
+        <initiator-group-name>%(initiator-group-name)s</initiator-group-name>
+        <initiator-group-type>%(initiator-group-type)s</initiator-group-type>
+        <initiator-group-uuid>1477ee47-0e1f-4b35-a82c-dcca0b76fc44
+        </initiator-group-uuid>
+        <initiator-group-os-type>linux</initiator-group-os-type>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-throttle-borrow>false
+        </initiator-group-throttle-borrow>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-report-scsi-name-enabled>true
+        </initiator-group-report-scsi-name-enabled>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiators>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c3</initiator-name>
+          </initiator-info>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c2</initiator-name>
+            <initiator-alias-info>
+              <initiator-alias>Centos</initiator-alias>
+            </initiator-alias-info>
+          </initiator-info>
+        </initiators>
+        <lun-id>2</lun-id>
+      </initiator-group-info>
+    </initiator-groups>
+  </results>""" % fake.IGROUP1))
+        initiators = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.get_lun_map.return_value = response
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup('path',
+                                                                initiators)
+
+        self.assertEqual(igroup, fake.IGROUP1_NAME)
+        self.assertEqual(lun_id, '2')
+
+    def test_find_mapped_lun_igroup_initiator_mismatch(self):
+        response = netapp_api.NaElement(etree.XML("""
+<results status="passed">
+    <initiator-groups>
+      <initiator-group-info>
+        <initiator-group-name>openstack-igroup1</initiator-group-name>
+        <initiator-group-type>fcp</initiator-group-type>
+        <initiator-group-uuid>1477ee47-0e1f-4b35-a82c-dcca0b76fc44
+        </initiator-group-uuid>
+        <initiator-group-os-type>linux</initiator-group-os-type>
+        <initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
+        <initiator-group-throttle-borrow>false
+        </initiator-group-throttle-borrow>
+        <initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
+        <initiator-group-alua-enabled>true</initiator-group-alua-enabled>
+        <initiator-group-report-scsi-name-enabled>true
+        </initiator-group-report-scsi-name-enabled>
+        <initiator-group-use-partner>true</initiator-group-use-partner>
+        <initiators>
+          <initiator-info>
+            <initiator-name>21:00:00:24:ff:40:6c:c3</initiator-name>
+          </initiator-info>
+        </initiators>
+        <lun-id>2</lun-id>
+      </initiator-group-info>
+    </initiator-groups>
+  </results>"""))
+        initiators = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.get_lun_map.return_value = response
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup('path',
+                                                                initiators)
+
+        self.assertIsNone(igroup)
+        self.assertIsNone(lun_id)
+
+    def test_find_mapped_lun_igroup_no_igroups(self):
+        response = netapp_api.NaElement(etree.XML("""
+  <results status="passed">
+    <initiator-groups />
+  </results>"""))
+        initiators = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.get_lun_map.return_value = response
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup('path',
+                                                                initiators)
+
+        self.assertIsNone(igroup)
+        self.assertIsNone(lun_id)
+
+    def test_find_mapped_lun_igroup_raises(self):
+        self.zapi_client.get_lun_map.side_effect = NaApiError
+        initiators = fake.FC_FORMATTED_INITIATORS
+        self.assertRaises(NaApiError,
+                          self.library._find_mapped_lun_igroup,
+                          'path',
+                          initiators)
+
+    def test_has_luns_mapped_to_initiators_local_map(self):
+        initiator_list = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.has_luns_mapped_to_initiators.return_value = True
+        self.library.partner_zapi_client = mock.Mock()
+
+        result = self.library._has_luns_mapped_to_initiators(initiator_list)
+
+        self.assertTrue(result)
+        self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with(
+            initiator_list)
+        self.assertEqual(0, self.library.partner_zapi_client.
+                         has_luns_mapped_to_initiators.call_count)
+
+    def test_has_luns_mapped_to_initiators_partner_map(self):
+        initiator_list = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.has_luns_mapped_to_initiators.return_value = False
+        self.library.partner_zapi_client = mock.Mock()
+        self.library.partner_zapi_client.has_luns_mapped_to_initiators.\
+            return_value = True
+
+        result = self.library._has_luns_mapped_to_initiators(initiator_list)
+
+        self.assertTrue(result)
+        self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with(
+            initiator_list)
+        self.library.partner_zapi_client.has_luns_mapped_to_initiators.\
+            assert_called_with(initiator_list)
+
+    def test_has_luns_mapped_to_initiators_no_maps(self):
+        initiator_list = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.has_luns_mapped_to_initiators.return_value = False
+        self.library.partner_zapi_client = mock.Mock()
+        self.library.partner_zapi_client.has_luns_mapped_to_initiators.\
+            return_value = False
+
+        result = self.library._has_luns_mapped_to_initiators(initiator_list)
+
+        self.assertFalse(result)
+        self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with(
+            initiator_list)
+        self.library.partner_zapi_client.has_luns_mapped_to_initiators.\
+            assert_called_with(initiator_list)
+
+    def test_has_luns_mapped_to_initiators_no_partner(self):
+        initiator_list = fake.FC_FORMATTED_INITIATORS
+        self.zapi_client.has_luns_mapped_to_initiators.return_value = False
+        self.library.partner_zapi_client = mock.Mock()
+        self.library.partner_zapi_client.has_luns_mapped_to_initiators.\
+            return_value = True
+
+        result = self.library._has_luns_mapped_to_initiators(
+            initiator_list, include_partner=False)
+
+        self.assertFalse(result)
+        self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with(
+            initiator_list)
+        self.assertEqual(0, self.library.partner_zapi_client.
+                         has_luns_mapped_to_initiators.call_count)
+
     def test_clone_lun_zero_block_count(self):
         """Test for when clone lun is not passed a block count."""
 
@@ -88,22 +304,51 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
             '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN',
             'newFakeLUN', 'true', block_count=0, dest_block=0, src_block=0)
 
+    def test_get_fc_target_wwpns(self):
+        ports1 = [fake.FC_FORMATTED_TARGET_WWPNS[0],
+                  fake.FC_FORMATTED_TARGET_WWPNS[1]]
+        ports2 = [fake.FC_FORMATTED_TARGET_WWPNS[2],
+                  fake.FC_FORMATTED_TARGET_WWPNS[3]]
+        self.zapi_client.get_fc_target_wwpns.return_value = ports1
+        self.library.partner_zapi_client = mock.Mock()
+        self.library.partner_zapi_client.get_fc_target_wwpns.return_value = \
+            ports2
+
+        result = self.library._get_fc_target_wwpns()
+
+        self.assertSetEqual(set(fake.FC_FORMATTED_TARGET_WWPNS), set(result))
+
+    def test_get_fc_target_wwpns_no_partner(self):
+        ports1 = [fake.FC_FORMATTED_TARGET_WWPNS[0],
+                  fake.FC_FORMATTED_TARGET_WWPNS[1]]
+        ports2 = [fake.FC_FORMATTED_TARGET_WWPNS[2],
+                  fake.FC_FORMATTED_TARGET_WWPNS[3]]
+        self.zapi_client.get_fc_target_wwpns.return_value = ports1
+        self.library.partner_zapi_client = mock.Mock()
+        self.library.partner_zapi_client.get_fc_target_wwpns.return_value = \
+            ports2
+
+        result = self.library._get_fc_target_wwpns(include_partner=False)
+
+        self.assertSetEqual(set(ports1), set(result))
+
     @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary,
                        '_refresh_volume_info', mock.Mock())
     @mock.patch.object(block_7mode.NetAppBlockStorage7modeLibrary,
                        '_get_pool_stats', mock.Mock())
     def test_vol_stats_calls_provide_ems(self):
         self.library.zapi_client.provide_ems = mock.Mock()
+
         self.library.get_volume_stats(refresh=True)
+
         self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1)
 
     def test_create_lun(self):
         self.library.vol_refresh_voluntary = False
 
-        self.library._create_lun(FAKE_VOLUME, FAKE_LUN, FAKE_SIZE,
-                                 FAKE_METADATA)
+        self.library._create_lun(fake.VOLUME, fake.LUN,
+                                 fake.SIZE, fake.METADATA)
 
         self.library.zapi_client.create_lun.assert_called_once_with(
-            FAKE_VOLUME, FAKE_LUN, FAKE_SIZE, FAKE_METADATA, None)
-
+            fake.VOLUME, fake.LUN, fake.SIZE, fake.METADATA, None)
         self.assertTrue(self.library.vol_refresh_voluntary)
index 62787837a82ae25ac5cab6d5d03a63872a11e10b..392db4315d0662847b210ae29138d5e8d33cf07c 100644 (file)
@@ -1,6 +1,5 @@
 # Copyright (c) 2014 Alex Meade.  All rights reserved.
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
-# 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
 Mock unit tests for the NetApp block storage library
 """
 
+
 import uuid
 
 import mock
 
 from cinder import exception
 from cinder import test
+from cinder.tests.volume.drivers.netapp.dataontap import fakes as fake
 from cinder.volume.drivers.netapp.dataontap import block_base
+from cinder.volume.drivers.netapp.dataontap.block_base import \
+    NetAppBlockStorageLibrary as block_lib
+from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError
 from cinder.volume.drivers.netapp import utils as na_utils
 
 
@@ -33,52 +37,45 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
         super(NetAppBlockStorageLibraryTestCase, self).setUp()
 
         kwargs = {'configuration': mock.Mock()}
-        self.library = block_base.NetAppBlockStorageLibrary('driver',
-                                                            'protocol',
-                                                            **kwargs)
+        self.library = block_lib('driver', 'protocol', **kwargs)
         self.library.zapi_client = mock.Mock()
+        self.zapi_client = self.library.zapi_client
         self.mock_request = mock.Mock()
 
     def tearDown(self):
         super(NetAppBlockStorageLibraryTestCase, self).tearDown()
 
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr',
+    @mock.patch.object(block_lib, '_get_lun_attr',
                        mock.Mock(return_value={'Volume': 'vol1'}))
     def test_get_pool(self):
         pool = self.library.get_pool({'name': 'volume-fake-uuid'})
         self.assertEqual(pool, 'vol1')
 
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr',
+    @mock.patch.object(block_lib, '_get_lun_attr',
                        mock.Mock(return_value=None))
     def test_get_pool_no_metadata(self):
         pool = self.library.get_pool({'name': 'volume-fake-uuid'})
         self.assertEqual(pool, None)
 
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_get_lun_attr',
+    @mock.patch.object(block_lib, '_get_lun_attr',
                        mock.Mock(return_value=dict()))
     def test_get_pool_volume_unknown(self):
         pool = self.library.get_pool({'name': 'volume-fake-uuid'})
         self.assertEqual(pool, None)
 
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary, '_create_lun',
-                       mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_create_lun_handle',
-                       mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_add_lun_to_table',
-                       mock.Mock())
+    @mock.patch.object(block_lib, '_create_lun', mock.Mock())
+    @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock())
+    @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock())
     @mock.patch.object(na_utils, 'get_volume_extra_specs',
                        mock.Mock(return_value=None))
-    @mock.patch.object(block_base, 'LOG',
-                       mock.Mock())
+    @mock.patch.object(block_base, 'LOG', mock.Mock())
     def test_create_volume(self):
         self.library.create_volume({'name': 'lun1', 'size': 100,
                                     'id': uuid.uuid4(),
                                     'host': 'hostname@backend#vol1'})
         self.library._create_lun.assert_called_once_with(
             'vol1', 'lun1', 107374182400, mock.ANY, None)
-        self.assertEqual(0, block_base.LOG.warn.call_count)
+        self.assertEqual(0, block_base.LOG.warning.call_count)
 
     def test_create_volume_no_pool_provided_by_scheduler(self):
         self.assertRaises(exception.InvalidHost, self.library.create_volume,
@@ -86,12 +83,206 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
                            'id': uuid.uuid4(),
                            'host': 'hostname@backend'})  # missing pool
 
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_create_lun', mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_create_lun_handle', mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_add_lun_to_table', mock.Mock())
+    @mock.patch.object(block_lib, '_get_lun_attr')
+    @mock.patch.object(block_lib, '_get_or_create_igroup')
+    def test_map_lun(self, mock_get_or_create_igroup, mock_get_lun_attr):
+        os = 'linux'
+        protocol = 'fcp'
+        mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os}
+        mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME
+        self.zapi_client.map_lun.return_value = '1'
+
+        lun_id = self.library._map_lun('fake_volume',
+                                       fake.FC_FORMATTED_INITIATORS,
+                                       protocol, None)
+
+        self.assertEqual(lun_id, '1')
+        mock_get_or_create_igroup.assert_called_once_with(
+            fake.FC_FORMATTED_INITIATORS, protocol, os)
+        self.zapi_client.map_lun.assert_called_once_with(
+            fake.LUN1, fake.IGROUP1_NAME, lun_id=None)
+
+    @mock.patch.object(block_lib, '_get_lun_attr')
+    @mock.patch.object(block_lib, '_get_or_create_igroup')
+    @mock.patch.object(block_lib, '_find_mapped_lun_igroup')
+    def test_map_lun_preexisting(self, mock_find_mapped_lun_igroup,
+                                 mock_get_or_create_igroup, mock_get_lun_attr):
+        os = 'linux'
+        protocol = 'fcp'
+        mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os}
+        mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME
+        mock_find_mapped_lun_igroup.return_value = (fake.IGROUP1_NAME, '2')
+        self.zapi_client.map_lun.side_effect = NaApiError
+
+        lun_id = self.library._map_lun(
+            'fake_volume', fake.FC_FORMATTED_INITIATORS, protocol, None)
+
+        self.assertEqual(lun_id, '2')
+        mock_find_mapped_lun_igroup.assert_called_once_with(
+            fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+    @mock.patch.object(block_lib, '_get_lun_attr')
+    @mock.patch.object(block_lib, '_get_or_create_igroup')
+    @mock.patch.object(block_lib, '_find_mapped_lun_igroup')
+    def test_map_lun_api_error(self, mock_find_mapped_lun_igroup,
+                               mock_get_or_create_igroup, mock_get_lun_attr):
+        os = 'linux'
+        protocol = 'fcp'
+        mock_get_lun_attr.return_value = {'Path': fake.LUN1, 'OsType': os}
+        mock_get_or_create_igroup.return_value = fake.IGROUP1_NAME
+        mock_find_mapped_lun_igroup.return_value = (None, None)
+        self.zapi_client.map_lun.side_effect = NaApiError
+
+        self.assertRaises(NaApiError, self.library._map_lun, 'fake_volume',
+                          fake.FC_FORMATTED_INITIATORS, protocol, None)
+
+    @mock.patch.object(block_lib, '_find_mapped_lun_igroup')
+    def test_unmap_lun(self, mock_find_mapped_lun_igroup):
+        mock_find_mapped_lun_igroup.return_value = (fake.IGROUP1_NAME, 1)
+
+        self.library._unmap_lun(fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+        self.zapi_client.unmap_lun.assert_called_once_with(fake.LUN1,
+                                                           fake.IGROUP1_NAME)
+
+    def test_find_mapped_lun_igroup(self):
+        self.assertRaises(NotImplementedError,
+                          self.library._find_mapped_lun_igroup,
+                          fake.LUN1,
+                          fake.FC_FORMATTED_INITIATORS)
+
+    def test_has_luns_mapped_to_initiators(self):
+        self.zapi_client.has_luns_mapped_to_initiators.return_value = True
+        self.assertTrue(self.library._has_luns_mapped_to_initiators(
+            fake.FC_FORMATTED_INITIATORS))
+        self.zapi_client.has_luns_mapped_to_initiators.assert_called_once_with(
+            fake.FC_FORMATTED_INITIATORS)
+
+    def test_get_or_create_igroup_preexisting(self):
+        self.zapi_client.get_igroup_by_initiators.return_value = [fake.IGROUP1]
+
+        igroup_name = self.library._get_or_create_igroup(
+            fake.FC_FORMATTED_INITIATORS, 'fcp', 'linux')
+
+        self.assertEqual(igroup_name, fake.IGROUP1_NAME)
+        self.zapi_client.get_igroup_by_initiators.assert_called_once_with(
+            fake.FC_FORMATTED_INITIATORS)
+
+    @mock.patch.object(uuid, 'uuid4', mock.Mock(return_value=fake.UUID1))
+    def test_get_or_create_igroup_none_preexisting(self):
+        self.zapi_client.get_igroup_by_initiators.return_value = []
+
+        igroup_name = self.library._get_or_create_igroup(
+            fake.FC_FORMATTED_INITIATORS, 'fcp', 'linux')
+
+        self.assertEqual(igroup_name, 'openstack-' + fake.UUID1)
+        self.zapi_client.create_igroup.assert_called_once_with(
+            igroup_name, 'fcp', 'linux')
+        self.assertEqual(len(fake.FC_FORMATTED_INITIATORS),
+                         self.zapi_client.add_igroup_initiator.call_count)
+
+    def test_get_fc_target_wwpns(self):
+        self.assertRaises(NotImplementedError,
+                          self.library._get_fc_target_wwpns)
+
+    @mock.patch.object(block_lib, '_build_initiator_target_map')
+    @mock.patch.object(block_lib, '_map_lun')
+    def test_initialize_connection_fc(self, mock_map_lun,
+                                      mock_build_initiator_target_map):
+        self.maxDiff = None
+        mock_map_lun.return_value = '1'
+        mock_build_initiator_target_map.return_value = (fake.FC_TARGET_WWPNS,
+                                                        fake.FC_I_T_MAP, 4)
+
+        target_info = self.library.initialize_connection_fc(fake.FC_VOLUME,
+                                                            fake.FC_CONNECTOR)
+
+        self.assertDictEqual(target_info, fake.FC_TARGET_INFO)
+        mock_map_lun.assert_called_once_with(
+            'fake_volume', fake.FC_FORMATTED_INITIATORS, 'fcp', None)
+
+    @mock.patch.object(block_lib, '_build_initiator_target_map')
+    @mock.patch.object(block_lib, '_map_lun')
+    def test_initialize_connection_fc_no_wwpns(
+            self, mock_map_lun, mock_build_initiator_target_map):
+
+        mock_map_lun.return_value = '1'
+        mock_build_initiator_target_map.return_value = (None, None, 0)
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.library.initialize_connection_fc,
+                          fake.FC_VOLUME,
+                          fake.FC_CONNECTOR)
+
+    @mock.patch.object(block_lib, '_has_luns_mapped_to_initiators')
+    @mock.patch.object(block_lib, '_unmap_lun')
+    @mock.patch.object(block_lib, '_get_lun_attr')
+    def test_terminate_connection_fc(self, mock_get_lun_attr, mock_unmap_lun,
+                                     mock_has_luns_mapped_to_initiators):
+
+        mock_get_lun_attr.return_value = {'Path': fake.LUN1}
+        mock_unmap_lun.return_value = None
+        mock_has_luns_mapped_to_initiators.return_value = True
+
+        target_info = self.library.terminate_connection_fc(fake.FC_VOLUME,
+                                                           fake.FC_CONNECTOR)
+
+        self.assertDictEqual(target_info, fake.FC_TARGET_INFO_EMPTY)
+        mock_unmap_lun.assert_called_once_with(fake.LUN1,
+                                               fake.FC_FORMATTED_INITIATORS)
+
+    @mock.patch.object(block_lib, '_build_initiator_target_map')
+    @mock.patch.object(block_lib, '_has_luns_mapped_to_initiators')
+    @mock.patch.object(block_lib, '_unmap_lun')
+    @mock.patch.object(block_lib, '_get_lun_attr')
+    def test_terminate_connection_fc_no_more_luns(
+            self, mock_get_lun_attr, mock_unmap_lun,
+            mock_has_luns_mapped_to_initiators,
+            mock_build_initiator_target_map):
+
+        mock_get_lun_attr.return_value = {'Path': fake.LUN1}
+        mock_unmap_lun.return_value = None
+        mock_has_luns_mapped_to_initiators.return_value = False
+        mock_build_initiator_target_map.return_value = (fake.FC_TARGET_WWPNS,
+                                                        fake.FC_I_T_MAP, 4)
+
+        target_info = self.library.terminate_connection_fc(fake.FC_VOLUME,
+                                                           fake.FC_CONNECTOR)
+
+        self.assertDictEqual(target_info, fake.FC_TARGET_INFO_UNMAP)
+
+    @mock.patch.object(block_lib, '_get_fc_target_wwpns')
+    def test_build_initiator_target_map_no_lookup_service(
+            self, mock_get_fc_target_wwpns):
+
+        self.library.lookup_service = None
+        mock_get_fc_target_wwpns.return_value = fake.FC_FORMATTED_TARGET_WWPNS
+
+        (target_wwpns, init_targ_map, num_paths) = \
+            self.library._build_initiator_target_map(fake.FC_CONNECTOR)
+
+        self.assertSetEqual(set(fake.FC_TARGET_WWPNS), set(target_wwpns))
+        self.assertDictEqual(fake.FC_I_T_MAP_COMPLETE, init_targ_map)
+        self.assertEqual(0, num_paths)
+
+    @mock.patch.object(block_lib, '_get_fc_target_wwpns')
+    def test_build_initiator_target_map_with_lookup_service(
+            self, mock_get_fc_target_wwpns):
+
+        self.library.lookup_service = mock.Mock()
+        self.library.lookup_service.get_device_mapping_from_network.\
+            return_value = fake.FC_FABRIC_MAP
+        mock_get_fc_target_wwpns.return_value = fake.FC_FORMATTED_TARGET_WWPNS
+
+        (target_wwpns, init_targ_map, num_paths) = \
+            self.library._build_initiator_target_map(fake.FC_CONNECTOR)
+
+        self.assertSetEqual(set(fake.FC_TARGET_WWPNS), set(target_wwpns))
+        self.assertDictEqual(fake.FC_I_T_MAP, init_targ_map)
+        self.assertEqual(4, num_paths)
+
+    @mock.patch.object(block_lib, '_create_lun', mock.Mock())
+    @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock())
+    @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock())
     @mock.patch.object(na_utils, 'LOG', mock.Mock())
     @mock.patch.object(na_utils, 'get_volume_extra_specs',
                        mock.Mock(return_value={'netapp:raid_type': 'raid4'}))
@@ -100,16 +291,14 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
         self.library.create_volume({'name': 'lun1', 'size': 100,
                                     'id': uuid.uuid4(),
                                     'host': 'hostname@backend#vol1'})
+
         warn_msg = 'Extra spec netapp:raid_type is obsolete.  ' \
                    'Use netapp_raid_type instead.'
-        na_utils.LOG.warn.assert_called_once_with(warn_msg)
-
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_create_lun', mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_create_lun_handle', mock.Mock())
-    @mock.patch.object(block_base.NetAppBlockStorageLibrary,
-                       '_add_lun_to_table', mock.Mock())
+        na_utils.LOG.warning.assert_called_once_with(warn_msg)
+
+    @mock.patch.object(block_lib, '_create_lun', mock.Mock())
+    @mock.patch.object(block_lib, '_create_lun_handle', mock.Mock())
+    @mock.patch.object(block_lib, '_add_lun_to_table', mock.Mock())
     @mock.patch.object(na_utils, 'LOG', mock.Mock())
     @mock.patch.object(na_utils, 'get_volume_extra_specs',
                        mock.Mock(return_value={'netapp_thick_provisioned':
@@ -119,6 +308,7 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
         self.library.create_volume({'name': 'lun1', 'size': 100,
                                     'id': uuid.uuid4(),
                                     'host': 'hostname@backend#vol1'})
+
         warn_msg = 'Extra spec netapp_thick_provisioned is deprecated.  ' \
                    'Use netapp_thin_provisioned instead.'
-        na_utils.LOG.warn.assert_called_once_with(warn_msg)
+        na_utils.LOG.warning.assert_called_once_with(warn_msg)
index 39f29648fe42d9d2a48569f933601b8b66502eea..8cf19a54949139de0a219c002407f68d5627f551 100644 (file)
@@ -1,6 +1,5 @@
 # Copyright (c) 2014 Alex Meade.  All rights reserved.
 # Copyright (c) 2014 Clinton Knight.  All rights reserved.
-# 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
 Mock unit tests for the NetApp block storage C-mode library
 """
 
-import uuid
 
 import mock
-import six
 
 from cinder import test
+import cinder.tests.volume.drivers.netapp.dataontap.fakes as fake
+import cinder.tests.volume.drivers.netapp.fakes as na_fakes
+from cinder.volume.drivers.netapp.dataontap.block_base import \
+    NetAppBlockStorageLibrary as block_lib
 from cinder.volume.drivers.netapp.dataontap import block_cmode
+from cinder.volume.drivers.netapp.dataontap.block_cmode import \
+    NetAppBlockStorageCmodeLibrary as block_lib_cmode
 from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
+from cinder.volume.drivers.netapp.dataontap.client import client_base
 from cinder.volume.drivers.netapp.dataontap import ssc_cmode
-
-FAKE_VOLUME = six.text_type(uuid.uuid4())
-FAKE_LUN = six.text_type(uuid.uuid4())
-FAKE_SIZE = '1024'
-FAKE_METADATA = {'OsType': 'linux', 'SpaceReserved': 'true'}
+from cinder.volume.drivers.netapp import utils as na_utils
 
 
 class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
@@ -39,17 +39,114 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
     def setUp(self):
         super(NetAppBlockStorageCmodeLibraryTestCase, self).setUp()
 
-        kwargs = {'configuration': mock.Mock()}
-        self.library = block_cmode.NetAppBlockStorageCmodeLibrary('driver',
-                                                                  'protocol',
-                                                                  **kwargs)
+        kwargs = {'configuration': self.get_config_cmode()}
+        self.library = block_lib_cmode('driver', 'protocol', **kwargs)
+
         self.library.zapi_client = mock.Mock()
+        self.zapi_client = self.library.zapi_client
         self.library.vserver = mock.Mock()
         self.library.ssc_vols = None
 
     def tearDown(self):
         super(NetAppBlockStorageCmodeLibraryTestCase, self).tearDown()
 
+    def get_config_cmode(self):
+        config = na_fakes.create_configuration_cmode()
+        config.netapp_storage_protocol = 'iscsi'
+        config.netapp_login = 'admin'
+        config.netapp_password = 'pass'
+        config.netapp_server_hostname = '127.0.0.1'
+        config.netapp_transport_type = 'https'
+        config.netapp_server_port = '443'
+        config.netapp_vserver = 'openstack'
+        return config
+
+    @mock.patch.object(client_base.Client, 'get_ontapi_version',
+                       mock.MagicMock(return_value=(1, 20)))
+    @mock.patch.object(na_utils, 'check_flags')
+    @mock.patch.object(block_lib, 'do_setup')
+    def test_do_setup(self, super_do_setup, mock_check_flags):
+        context = mock.Mock()
+
+        self.library.do_setup(context)
+
+        super_do_setup.assert_called_once_with(context)
+        self.assertEqual(1, mock_check_flags.call_count)
+
+    @mock.patch.object(block_lib, 'check_for_setup_error')
+    @mock.patch.object(ssc_cmode, 'check_ssc_api_permissions')
+    def test_check_for_setup_error(self, mock_check_ssc_api_permissions,
+                                   super_check_for_setup_error):
+
+        self.library.check_for_setup_error()
+
+        super_check_for_setup_error.assert_called_once_with()
+        mock_check_ssc_api_permissions.assert_called_once_with(
+            self.library.zapi_client)
+
+    def test_find_mapped_lun_igroup(self):
+        igroups = [fake.IGROUP1]
+        self.zapi_client.get_igroup_by_initiators.return_value = igroups
+
+        lun_maps = [{'initiator-group': fake.IGROUP1_NAME,
+                     'lun-id': '1',
+                     'vserver': fake.VSERVER1_NAME}]
+        self.zapi_client.get_lun_map.return_value = lun_maps
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup(
+            fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+        self.assertEqual(fake.IGROUP1_NAME, igroup)
+        self.assertEqual('1', lun_id)
+
+    def test_find_mapped_lun_igroup_initiator_mismatch(self):
+        self.zapi_client.get_igroup_by_initiators.return_value = []
+
+        lun_maps = [{'initiator-group': fake.IGROUP1_NAME,
+                     'lun-id': '1',
+                     'vserver': fake.VSERVER1_NAME}]
+        self.zapi_client.get_lun_map.return_value = lun_maps
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup(
+            fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+        self.assertIsNone(igroup)
+        self.assertIsNone(lun_id)
+
+    def test_find_mapped_lun_igroup_name_mismatch(self):
+        igroups = [{'initiator-group-os-type': 'linux',
+                    'initiator-group-type': 'fcp',
+                    'initiator-group-name': 'igroup2'}]
+        self.zapi_client.get_igroup_by_initiators.return_value = igroups
+
+        lun_maps = [{'initiator-group': fake.IGROUP1_NAME,
+                     'lun-id': '1',
+                     'vserver': fake.VSERVER1_NAME}]
+        self.zapi_client.get_lun_map.return_value = lun_maps
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup(
+            fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+        self.assertIsNone(igroup)
+        self.assertIsNone(lun_id)
+
+    def test_find_mapped_lun_igroup_no_igroup_prefix(self):
+        igroups = [{'initiator-group-os-type': 'linux',
+                    'initiator-group-type': 'fcp',
+                    'initiator-group-name': 'igroup2'}]
+        self.zapi_client.get_igroup_by_initiators.return_value = igroups
+
+        lun_maps = [{'initiator-group': 'igroup2',
+                     'lun-id': '1',
+                     'vserver': fake.VSERVER1_NAME}]
+        self.zapi_client.get_lun_map.return_value = lun_maps
+
+        (igroup, lun_id) = self.library._find_mapped_lun_igroup(
+            fake.LUN1, fake.FC_FORMATTED_INITIATORS)
+
+        self.assertIsNone(igroup)
+        self.assertIsNone(lun_id)
+
     def test_clone_lun_zero_block_count(self):
         """Test for when clone lun is not passed a block count."""
 
@@ -92,24 +189,31 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
             'fakeLUN', 'fakeLUN', 'newFakeLUN', 'true', block_count=0,
             dest_block=0, src_block=0)
 
+    def test_get_fc_target_wwpns(self):
+        ports = [fake.FC_FORMATTED_TARGET_WWPNS[0],
+                 fake.FC_FORMATTED_TARGET_WWPNS[1]]
+        self.zapi_client.get_fc_target_wwpns.return_value = ports
+
+        result = self.library._get_fc_target_wwpns()
+
+        self.assertSetEqual(set(ports), set(result))
+
     @mock.patch.object(ssc_cmode, 'refresh_cluster_ssc', mock.Mock())
     @mock.patch.object(block_cmode.NetAppBlockStorageCmodeLibrary,
                        '_get_pool_stats', mock.Mock())
     def test_vol_stats_calls_provide_ems(self):
         self.library.zapi_client.provide_ems = mock.Mock()
+
         self.library.get_volume_stats(refresh=True)
+
         self.assertEqual(self.library.zapi_client.provide_ems.call_count, 1)
 
     def test_create_lun(self):
         self.library._update_stale_vols = mock.Mock()
 
-        self.library._create_lun(FAKE_VOLUME,
-                                 FAKE_LUN,
-                                 FAKE_SIZE,
-                                 FAKE_METADATA)
+        self.library._create_lun(fake.VOLUME, fake.LUN,
+                                 fake.SIZE, fake.METADATA)
 
         self.library.zapi_client.create_lun.assert_called_once_with(
-            FAKE_VOLUME, FAKE_LUN, FAKE_SIZE,
-            FAKE_METADATA, None)
-
+            fake.VOLUME, fake.LUN, fake.SIZE, fake.METADATA, None)
         self.assertEqual(1, self.library._update_stale_vols.call_count)
diff --git a/cinder/tests/volume/drivers/netapp/eseries/__init__.py b/cinder/tests/volume/drivers/netapp/eseries/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cinder/tests/volume/drivers/netapp/fakes.py b/cinder/tests/volume/drivers/netapp/fakes.py
new file mode 100644 (file)
index 0000000..d39a52d
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright (c) - 2014, Clinton Knight.  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.
+
+
+from cinder.volume import configuration as conf
+import cinder.volume.drivers.netapp.options as na_opts
+
+
+def create_configuration():
+    config = conf.Configuration(None)
+    config.append_config_values(na_opts.netapp_connection_opts)
+    config.append_config_values(na_opts.netapp_transport_opts)
+    config.append_config_values(na_opts.netapp_basicauth_opts)
+    config.append_config_values(na_opts.netapp_provisioning_opts)
+    return config
+
+
+def create_configuration_7mode():
+    config = create_configuration()
+    config.append_config_values(na_opts.netapp_7mode_opts)
+    return config
+
+
+def create_configuration_cmode():
+    config = create_configuration()
+    config.append_config_values(na_opts.netapp_cluster_opts)
+    return config
+
+
+def create_configuration_eseries():
+    config = create_configuration()
+    config.append_config_values(na_opts.netapp_eseries_opts)
+    return config
index 36e2b7ef2320e1a260ea0d76927e7b0df9b2d1f6..70415b73df1f1ccc6e00961ecc676bb213508b10 100644 (file)
@@ -42,12 +42,14 @@ netapp_unified_plugin_registry =\
     {'ontap_cluster':
      {
          'iscsi': DATAONTAP_PATH + '.iscsi_cmode.NetAppCmodeISCSIDriver',
-         'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver'
+         'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver',
+         'fc': DATAONTAP_PATH + '.fc_cmode.NetAppCmodeFibreChannelDriver'
      },
      'ontap_7mode':
      {
          'iscsi': DATAONTAP_PATH + '.iscsi_7mode.NetApp7modeISCSIDriver',
-         'nfs': DATAONTAP_PATH + '.nfs_7mode.NetApp7modeNfsDriver'
+         'nfs': DATAONTAP_PATH + '.nfs_7mode.NetApp7modeNfsDriver',
+         'fc': DATAONTAP_PATH + '.fc_7mode.NetApp7modeFibreChannelDriver'
      },
      'eseries':
      {
@@ -123,8 +125,7 @@ class NetAppDriverFactory(object):
         if driver_loc is None:
             raise exception.InvalidInput(
                 reason=_('Protocol %(storage_protocol)s is not supported'
-                         ' for storage family %(storage_family)s')
-                % fmt)
+                         ' for storage family %(storage_family)s') % fmt)
 
         NetAppDriverFactory.check_netapp_driver(driver_loc)
         kwargs = kwargs or {}
index a83776f62a355baffed9c558eb7e671874d50e1f..bea12f382548844f79c72e25ca357f9705bb408c 100644 (file)
@@ -28,6 +28,7 @@ import six
 from cinder import exception
 from cinder.i18n import _, _LW
 from cinder.openstack.common import log as logging
+from cinder.volume.configuration import Configuration
 from cinder.volume.drivers.netapp.dataontap import block_base
 from cinder.volume.drivers.netapp.dataontap.client import client_7mode
 from cinder.volume.drivers.netapp import options as na_opts
@@ -67,12 +68,31 @@ class NetAppBlockStorage7modeLibrary(block_base.
             port=self.configuration.netapp_server_port,
             vfiler=self.vfiler)
 
+        self._do_partner_setup()
+
         self.vol_refresh_time = None
         self.vol_refresh_interval = 1800
         self.vol_refresh_running = False
         self.vol_refresh_voluntary = False
         self.root_volume_name = self._get_root_volume_name()
 
+    def _do_partner_setup(self):
+        partner_backend = self.configuration.netapp_partner_backend_name
+        if partner_backend:
+            config = Configuration(na_opts.netapp_7mode_opts, partner_backend)
+            config.append_config_values(na_opts.netapp_connection_opts)
+            config.append_config_values(na_opts.netapp_basicauth_opts)
+            config.append_config_values(na_opts.netapp_transport_opts)
+
+            self.partner_zapi_client = client_7mode.Client(
+                None,
+                transport_type=config.netapp_transport_type,
+                username=config.netapp_login,
+                password=config.netapp_password,
+                hostname=config.netapp_server_hostname,
+                port=config.netapp_server_port,
+                vfiler=None)
+
     def check_for_setup_error(self):
         """Check that the driver is working and can communicate."""
         api_version = self.zapi_client.get_ontapi_version()
@@ -120,27 +140,40 @@ class NetAppBlockStorage7modeLibrary(block_base.
         owner = self._get_owner()
         return '%s:%s' % (owner, metadata['Path'])
 
-    def _find_mapped_lun_igroup(self, path, initiator, os=None):
-        """Find the igroup for mapped LUN with initiator."""
-        igroup = None
-        lun_id = None
+    def _find_mapped_lun_igroup(self, path, initiator_list):
+        """Find an igroup for a LUN mapped to the given initiator(s)."""
+        initiator_set = set(initiator_list)
+
         result = self.zapi_client.get_lun_map(path)
-        igroups = result.get_child_by_name('initiator-groups')
-        if igroups:
-            found = False
-            igroup_infs = igroups.get_children()
-            for ig in igroup_infs:
-                initiators = ig.get_child_by_name('initiators')
-                init_infs = initiators.get_children()
-                for info in init_infs:
-                    if info.get_child_content('initiator-name') == initiator:
-                        found = True
-                        igroup = ig.get_child_content('initiator-group-name')
-                        lun_id = ig.get_child_content('lun-id')
-                        break
-                if found:
-                    break
-        return igroup, lun_id
+        initiator_groups = result.get_child_by_name('initiator-groups')
+        if initiator_groups:
+            for initiator_group_info in initiator_groups.get_children():
+
+                initiator_set_for_igroup = set()
+                for initiator_info in initiator_group_info.get_child_by_name(
+                        'initiators').get_children():
+                    initiator_set_for_igroup.add(
+                        initiator_info.get_child_content('initiator-name'))
+
+                if initiator_set == initiator_set_for_igroup:
+                        igroup = initiator_group_info.get_child_content(
+                            'initiator-group-name')
+                        lun_id = initiator_group_info.get_child_content(
+                            'lun-id')
+                        return igroup, lun_id
+
+        return None, None
+
+    def _has_luns_mapped_to_initiators(self, initiator_list,
+                                       include_partner=True):
+        """Checks whether any LUNs are mapped to the given initiator(s)."""
+        if self.zapi_client.has_luns_mapped_to_initiators(initiator_list):
+            return True
+        if include_partner and self.partner_zapi_client and \
+                self.partner_zapi_client.has_luns_mapped_to_initiators(
+                    initiator_list):
+            return True
+        return False
 
     def _clone_lun(self, name, new_name, space_reserved='true',
                    src_block=0, dest_block=0, block_count=0):
@@ -176,6 +209,12 @@ class NetAppBlockStorage7modeLibrary(block_base.
             'is-space-reservation-enabled')
         return meta_dict
 
+    def _get_fc_target_wwpns(self, include_partner=True):
+        wwpns = self.zapi_client.get_fc_target_wwpns()
+        if include_partner and self.partner_zapi_client:
+            wwpns.extend(self.partner_zapi_client.get_fc_target_wwpns())
+        return wwpns
+
     def _update_volume_stats(self):
         """Retrieve stats info from filer."""
 
index 5d2c7a2a7fbaaf2ec7343a9991df16037fd3481a..ecbed6eb8e546c8aa0d43fe0e3fc2d9752d1a37c 100644 (file)
@@ -35,6 +35,7 @@ from cinder.volume.drivers.netapp.dataontap.client.api import NaApiError
 from cinder.volume.drivers.netapp import options as na_opts
 from cinder.volume.drivers.netapp import utils as na_utils
 from cinder.volume import utils as volume_utils
+from cinder.zonemanager import utils as fczm_utils
 
 
 LOG = logging.getLogger(__name__)
@@ -81,6 +82,7 @@ class NetAppBlockStorageLibrary(object):
         self.zapi_client = None
         self._stats = {}
         self.lun_table = {}
+        self.lookup_service = fczm_utils.create_lookup_service()
         self.app_version = kwargs.get("app_version", "unknown")
 
         self.configuration = kwargs['configuration']
@@ -232,10 +234,7 @@ class NetAppBlockStorageLibrary(object):
         raise NotImplementedError()
 
     def _extract_and_populate_luns(self, api_luns):
-        """Extracts the LUNs from API.
-
-        Populates in the LUN table.
-        """
+        """Extracts the LUNs from API and populates the LUN table."""
 
         for lun in api_luns:
             meta_dict = self._create_lun_meta(lun)
@@ -246,8 +245,8 @@ class NetAppBlockStorageLibrary(object):
             discovered_lun = NetAppLun(handle, name, size, meta_dict)
             self._add_lun_to_table(discovered_lun)
 
-    def _map_lun(self, name, initiator, initiator_type='iscsi', lun_id=None):
-        """Maps LUN to the initiator and returns LUN id assigned."""
+    def _map_lun(self, name, initiator_list, initiator_type, lun_id=None):
+        """Maps LUN to the initiator(s) and returns LUN ID assigned."""
         metadata = self._get_lun_attr(name, 'metadata')
         os = metadata['OsType']
         path = metadata['Path']
@@ -255,35 +254,42 @@ class NetAppBlockStorageLibrary(object):
             os = os
         else:
             os = 'default'
-        igroup_name = self._get_or_create_igroup(initiator,
+        igroup_name = self._get_or_create_igroup(initiator_list,
                                                  initiator_type, os)
         try:
             return self.zapi_client.map_lun(path, igroup_name, lun_id=lun_id)
         except NaApiError:
             exc_info = sys.exc_info()
-            (_igroup, lun_id) = self._find_mapped_lun_igroup(path, initiator)
+            (_igroup, lun_id) = self._find_mapped_lun_igroup(path,
+                                                             initiator_list)
             if lun_id is not None:
                 return lun_id
             else:
                 raise exc_info[0], exc_info[1], exc_info[2]
 
-    def _unmap_lun(self, path, initiator):
+    def _unmap_lun(self, path, initiator_list):
         """Unmaps a LUN from given initiator."""
-        (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path, initiator)
+        (igroup_name, _lun_id) = self._find_mapped_lun_igroup(path,
+                                                              initiator_list)
         self.zapi_client.unmap_lun(path, igroup_name)
 
-    def _find_mapped_lun_igroup(self, path, initiator, os=None):
-        """Find the igroup for mapped LUN with initiator."""
+    def _find_mapped_lun_igroup(self, path, initiator_list):
+        """Find an igroup for a LUN mapped to the given initiator(s)."""
         raise NotImplementedError()
 
-    def _get_or_create_igroup(self, initiator, initiator_type='iscsi',
+    def _has_luns_mapped_to_initiators(self, initiator_list):
+        """Checks whether any LUNs are mapped to the given initiator(s)."""
+        return self.zapi_client.has_luns_mapped_to_initiators(initiator_list)
+
+    def _get_or_create_igroup(self, initiator_list, initiator_type,
                               os='default'):
-        """Checks for an igroup for an initiator.
+        """Checks for an igroup for a set of one or more initiators.
 
         Creates igroup if not found.
         """
 
-        igroups = self.zapi_client.get_igroup_by_initiator(initiator=initiator)
+        igroups = self.zapi_client.get_igroup_by_initiators(initiator_list)
+
         igroup_name = None
         for igroup in igroups:
             if igroup['initiator-group-os-type'] == os:
@@ -296,7 +302,8 @@ class NetAppBlockStorageLibrary(object):
         if not igroup_name:
             igroup_name = self.IGROUP_PREFIX + six.text_type(uuid.uuid4())
             self.zapi_client.create_igroup(igroup_name, initiator_type, os)
-            self.zapi_client.add_igroup_initiator(igroup_name, initiator)
+            for initiator in initiator_list:
+                self.zapi_client.add_igroup_initiator(igroup_name, initiator)
         return igroup_name
 
     def _check_allowed_os(self, os):
@@ -348,6 +355,9 @@ class NetAppBlockStorageLibrary(object):
     def _create_lun_meta(self, lun):
         raise NotImplementedError()
 
+    def _get_fc_target_wwpns(self, include_partner=True):
+        raise NotImplementedError()
+
     def create_cloned_volume(self, volume, src_vref):
         """Creates a clone of the specified volume."""
         vol_size = volume['size']
@@ -505,12 +515,12 @@ class NetAppBlockStorageLibrary(object):
 
         initiator_name = connector['initiator']
         name = volume['name']
-        lun_id = self._map_lun(name, initiator_name, 'iscsi', None)
+        lun_id = self._map_lun(name, [initiator_name], 'iscsi', None)
         msg = _("Mapped LUN %(name)s to the initiator %(initiator_name)s")
         msg_fmt = {'name': name, 'initiator_name': initiator_name}
         LOG.debug(msg % msg_fmt)
         iqn = self.zapi_client.get_iscsi_service_details()
-        target_details_list = self.zapi_client.get_target_details()
+        target_details_list = self.zapi_client.get_iscsi_target_details()
         msg = _("Successfully fetched target details for LUN %(name)s and "
                 "initiator %(initiator_name)s")
         msg_fmt = {'name': name, 'initiator_name': initiator_name}
@@ -565,7 +575,162 @@ class NetAppBlockStorageLibrary(object):
         name = volume['name']
         metadata = self._get_lun_attr(name, 'metadata')
         path = metadata['Path']
-        self._unmap_lun(path, initiator_name)
+        self._unmap_lun(path, [initiator_name])
         msg = _("Unmapped LUN %(name)s from the initiator %(initiator_name)s")
         msg_fmt = {'name': name, 'initiator_name': initiator_name}
         LOG.debug(msg % msg_fmt)
+
+    def initialize_connection_fc(self, volume, connector):
+        """Initializes the connection and returns connection info.
+
+        Assign any created volume to a compute node/host so that it can be
+        used from that host.
+
+        The driver returns a driver_volume_type of 'fibre_channel'.
+        The target_wwn can be a single entry or a list of wwns that
+        correspond to the list of remote wwn(s) that will export the volume.
+        Example return values:
+            {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': '500a098280feeba5',
+                    'access_mode': 'rw',
+                    'initiator_target_map': {
+                        '21000024ff406cc3': ['500a098280feeba5'],
+                        '21000024ff406cc2': ['500a098280feeba5']
+                    }
+                }
+            }
+
+            or
+
+             {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': ['500a098280feeba5', '500a098290feeba5',
+                                   '500a098190feeba5', '500a098180feeba5'],
+                    'access_mode': 'rw',
+                    'initiator_target_map': {
+                        '21000024ff406cc3': ['500a098280feeba5',
+                                             '500a098290feeba5'],
+                        '21000024ff406cc2': ['500a098190feeba5',
+                                             '500a098180feeba5']
+                    }
+                }
+            }
+        """
+
+        initiators = [fczm_utils.get_formatted_wwn(wwpn)
+                      for wwpn in connector['wwpns']]
+        volume_name = volume['name']
+
+        lun_id = self._map_lun(volume_name, initiators, 'fcp', None)
+
+        msg = _("Mapped LUN %(name)s to the initiator(s) %(initiators)s")
+        msg_fmt = {'name': volume_name, 'initiators': initiators}
+        LOG.debug(msg % msg_fmt)
+
+        target_wwpns, initiator_target_map, num_paths = \
+            self._build_initiator_target_map(connector)
+
+        if target_wwpns:
+            msg = _("Successfully fetched target details for LUN %(name)s "
+                    "and initiator(s) %(initiators)s")
+            msg_fmt = {'name': volume_name, 'initiators': initiators}
+            LOG.debug(msg % msg_fmt)
+        else:
+            msg = _('Failed to get LUN target details for the LUN %s')
+            raise exception.VolumeBackendAPIException(data=msg % volume_name)
+
+        target_info = {'driver_volume_type': 'fibre_channel',
+                       'data': {'target_discovered': True,
+                                'target_lun': lun_id,
+                                'target_wwn': target_wwpns,
+                                'access_mode': 'rw',
+                                'initiator_target_map': initiator_target_map}}
+
+        return target_info
+
+    def terminate_connection_fc(self, volume, connector, **kwargs):
+        """Disallow connection from connector.
+
+        Return empty data if other volumes are in the same zone.
+        The FibreChannel ZoneManager doesn't remove zones
+        if there isn't an initiator_target_map in the
+        return of terminate_connection.
+
+        :returns: data - the target_wwns and initiator_target_map if the
+                         zone is to be removed, otherwise the same map with
+                         an empty dict for the 'data' key
+        """
+
+        initiators = [fczm_utils.get_formatted_wwn(wwpn)
+                      for wwpn in connector['wwpns']]
+        name = volume['name']
+        metadata = self._get_lun_attr(name, 'metadata')
+        path = metadata['Path']
+
+        self._unmap_lun(path, initiators)
+
+        msg = _("Unmapped LUN %(name)s from the initiator %(initiators)s")
+        msg_fmt = {'name': name, 'initiators': initiators}
+        LOG.debug(msg % msg_fmt)
+
+        info = {'driver_volume_type': 'fibre_channel',
+                'data': {}}
+
+        if not self._has_luns_mapped_to_initiators(initiators):
+            # No more exports for this host, so tear down zone.
+            LOG.info(_LI("Need to remove FC Zone, building initiator "
+                         "target map"))
+
+            target_wwpns, initiator_target_map, num_paths = \
+                self._build_initiator_target_map(connector)
+
+            info['data'] = {'target_wwn': target_wwpns,
+                            'initiator_target_map': initiator_target_map}
+
+        return info
+
+    def _build_initiator_target_map(self, connector):
+        """Build the target_wwns and the initiator target map."""
+
+        # get WWPNs from controller and strip colons
+        all_target_wwpns = self._get_fc_target_wwpns()
+        all_target_wwpns = [six.text_type(wwpn).replace(':', '')
+                            for wwpn in all_target_wwpns]
+
+        target_wwpns = []
+        init_targ_map = {}
+        num_paths = 0
+
+        if self.lookup_service is not None:
+            # Use FC SAN lookup to determine which ports are visible.
+            dev_map = self.lookup_service.get_device_mapping_from_network(
+                connector['wwpns'],
+                all_target_wwpns)
+
+            for fabric_name in dev_map:
+                fabric = dev_map[fabric_name]
+                target_wwpns += fabric['target_port_wwn_list']
+                for initiator in fabric['initiator_port_wwn_list']:
+                    if initiator not in init_targ_map:
+                        init_targ_map[initiator] = []
+                    init_targ_map[initiator] += fabric['target_port_wwn_list']
+                    init_targ_map[initiator] = list(set(
+                        init_targ_map[initiator]))
+                    for target in init_targ_map[initiator]:
+                        num_paths += 1
+            target_wwpns = list(set(target_wwpns))
+        else:
+            initiator_wwns = connector['wwpns']
+            target_wwpns = all_target_wwpns
+
+            for initiator in initiator_wwns:
+                init_targ_map[initiator] = target_wwpns
+
+        return target_wwpns, init_targ_map, num_paths
index 774b4c0a7538c6c8c3c6ab0955f0eeb1011cd60c..8ec2e22135cfae5a15f3994736914dbebe7beb79 100644 (file)
@@ -89,10 +89,10 @@ class NetAppBlockStorageCmodeLibrary(block_base.
         """Returns LUN handle based on filer type."""
         return '%s:%s' % (self.vserver, metadata['Path'])
 
-    def _find_mapped_lun_igroup(self, path, initiator, os=None):
-        """Find the igroup for mapped LUN with initiator."""
-        initiator_igroups = self.zapi_client.get_igroup_by_initiator(
-            initiator=initiator)
+    def _find_mapped_lun_igroup(self, path, initiator_list):
+        """Find an igroup for a LUN mapped to the given initiator(s)."""
+        initiator_igroups = self.zapi_client.get_igroup_by_initiators(
+            initiator_list)
         lun_maps = self.zapi_client.get_lun_map(path)
         if initiator_igroups and lun_maps:
             for igroup in initiator_igroups:
@@ -140,6 +140,9 @@ class NetAppBlockStorageCmodeLibrary(block_base.
             lun.get_child_content('is-space-reservation-enabled')
         return meta_dict
 
+    def _get_fc_target_wwpns(self, include_partner=True):
+        return self.zapi_client.get_fc_target_wwpns()
+
     def _configure_tunneling(self, do_tunneling=False):
         """Configures tunneling for Data ONTAP cluster."""
         if do_tunneling:
index 6cbcd391959f05a04806b6a8dc5f13218ec8f1cf..207a1a0f703dbfdbfce5893d50c491513221ed68 100644 (file)
@@ -48,8 +48,41 @@ class Client(client_base.Client):
         result = server.invoke_successfully(na_element, True)
         return result
 
-    def get_target_details(self):
-        """Gets the target portal details."""
+    def _invoke_7mode_iterator_getter(self, start_api_name, next_api_name,
+                                      end_api_name, record_container_tag_name,
+                                      maximum=100):
+        """Invoke a 7-mode iterator-style getter API."""
+        data = []
+
+        start_api = netapp_api.NaElement(start_api_name)
+        start_result = self.connection.invoke_successfully(start_api)
+        tag = start_result.get_child_content('tag')
+        if not tag:
+            return data
+
+        try:
+            while True:
+                next_api = netapp_api.NaElement(next_api_name)
+                next_api.add_new_child('tag', tag)
+                next_api.add_new_child('maximum', six.text_type(maximum))
+                next_result = self.connection.invoke_successfully(next_api)
+                records = next_result.get_child_content('records') or 0
+                if int(records) == 0:
+                    break
+
+                record_container = next_result.get_child_by_name(
+                    record_container_tag_name) or netapp_api.NaElement('none')
+
+                data.extend(record_container.get_children())
+        finally:
+            end_api = netapp_api.NaElement(end_api_name)
+            end_api.add_new_child('tag', tag)
+            self.connection.invoke_successfully(end_api)
+
+        return data
+
+    def get_iscsi_target_details(self):
+        """Gets the iSCSI target portal details."""
         iscsi_if_iter = netapp_api.NaElement('iscsi-portal-list-info')
         result = self.connection.invoke_successfully(iscsi_if_iter, True)
         tgt_list = []
@@ -65,6 +98,18 @@ class Client(client_base.Client):
                 tgt_list.append(d)
         return tgt_list
 
+    def get_fc_target_wwpns(self):
+        """Gets the FC target details."""
+        wwpns = []
+        port_name_list_api = netapp_api.NaElement('fcp-port-name-list-info')
+        result = self.connection.invoke_successfully(port_name_list_api)
+        port_names = result.get_child_by_name('fcp-port-names')
+        if port_names:
+            for port_name_info in port_names.get_children():
+                wwpn = port_name_info.get_child_content('port-name').lower()
+                wwpns.append(wwpn)
+        return wwpns
+
     def get_iscsi_service_details(self):
         """Returns iscsi iqn."""
         iscsi_service_iter = netapp_api.NaElement('iscsi-node-get-name')
@@ -97,34 +142,41 @@ class Client(client_base.Client):
         luns = result.get_child_by_name('luns')
         return luns.get_children()
 
-    def get_igroup_by_initiator(self, initiator):
-        """Get igroups by initiator."""
-        igroup_list = netapp_api.NaElement('igroup-list-info')
-        result = self.connection.invoke_successfully(igroup_list, True)
-        igroups = []
-        igs = result.get_child_by_name('initiator-groups')
-        if igs:
-            ig_infos = igs.get_children()
-            if ig_infos:
-                for info in ig_infos:
-                    initiators = info.get_child_by_name('initiators')
-                    init_infos = initiators.get_children()
-                    if init_infos:
-                        for init in init_infos:
-                            if init.get_child_content('initiator-name')\
-                                    == initiator:
-                                d = dict()
-                                d['initiator-group-os-type'] = \
-                                    info.get_child_content(
-                                        'initiator-group-os-type')
-                                d['initiator-group-type'] = \
-                                    info.get_child_content(
-                                        'initiator-group-type')
-                                d['initiator-group-name'] = \
-                                    info.get_child_content(
-                                        'initiator-group-name')
-                                igroups.append(d)
-        return igroups
+    def get_igroup_by_initiators(self, initiator_list):
+        """Get igroups exactly matching a set of initiators."""
+        igroup_list = []
+        if not initiator_list:
+            return igroup_list
+
+        initiator_set = set(initiator_list)
+
+        igroup_list_info = netapp_api.NaElement('igroup-list-info')
+        result = self.connection.invoke_successfully(igroup_list_info, True)
+
+        initiator_groups = result.get_child_by_name(
+            'initiator-groups') or netapp_api.NaElement('none')
+        for initiator_group_info in initiator_groups.get_children():
+
+            initiator_set_for_igroup = set()
+            initiators = initiator_group_info.get_child_by_name(
+                'initiators') or netapp_api.NaElement('none')
+            for initiator_info in initiators.get_children():
+                initiator_set_for_igroup.add(
+                    initiator_info.get_child_content('initiator-name'))
+
+            if initiator_set == initiator_set_for_igroup:
+                igroup = {'initiator-group-os-type':
+                          initiator_group_info.get_child_content(
+                              'initiator-group-os-type'),
+                          'initiator-group-type':
+                          initiator_group_info.get_child_content(
+                              'initiator-group-type'),
+                          'initiator-group-name':
+                          initiator_group_info.get_child_content(
+                              'initiator-group-name')}
+                igroup_list.append(igroup)
+
+        return igroup_list
 
     def clone_lun(self, path, clone_path, name, new_name,
                   space_reserved='true', src_block=0,
index 965bed865fad04440927a05a71ec8812c1da344e..09811e3f89f83f4bb804dd94f9d851c3cbd96bde 100644 (file)
@@ -207,8 +207,12 @@ class Client(object):
         lun_move.add_new_child("new-path", new_path)
         self.connection.invoke_successfully(lun_move, True)
 
-    def get_target_details(self):
-        """Gets the target portal details."""
+    def get_iscsi_target_details(self):
+        """Gets the iSCSI target portal details."""
+        raise NotImplementedError()
+
+    def get_fc_target_wwpns(self):
+        """Gets the FC target details."""
         raise NotImplementedError()
 
     def get_iscsi_service_details(self):
@@ -219,10 +223,26 @@ class Client(object):
         """Gets the list of LUNs on filer."""
         raise NotImplementedError()
 
-    def get_igroup_by_initiator(self, initiator):
-        """Get igroups by initiator."""
+    def get_igroup_by_initiators(self, initiator_list):
+        """Get igroups exactly matching a set of initiators."""
         raise NotImplementedError()
 
+    def _has_luns_mapped_to_initiator(self, initiator):
+        """Checks whether any LUNs are mapped to the given initiator."""
+        lun_list_api = netapp_api.NaElement('lun-initiator-list-map-info')
+        lun_list_api.add_new_child('initiator', initiator)
+        result = self.connection.invoke_successfully(lun_list_api, True)
+        lun_maps_container = result.get_child_by_name(
+            'lun-maps') or netapp_api.NaElement('none')
+        return len(lun_maps_container.get_children()) > 0
+
+    def has_luns_mapped_to_initiators(self, initiator_list):
+        """Checks whether any LUNs are mapped to the given initiator(s)."""
+        for initiator in initiator_list:
+            if self._has_luns_mapped_to_initiator(initiator):
+                return True
+        return False
+
     def get_lun_by_args(self, **args):
         """Retrieves LUNs with specified args."""
         raise NotImplementedError()
index 2a83e6221fa19d5b23b2973d787212b1c03d8804..e63ee82a597e5ece6e3076a588b27c197a7718e9 100644 (file)
@@ -51,8 +51,8 @@ class Client(client_base.Client):
     def set_vserver(self, vserver):
         self.connection.set_vserver(vserver)
 
-    def get_target_details(self):
-        """Gets the target portal details."""
+    def get_iscsi_target_details(self):
+        """Gets the iSCSI target portal details."""
         iscsi_if_iter = netapp_api.NaElement('iscsi-interface-get-iter')
         result = self.connection.invoke_successfully(iscsi_if_iter, True)
         tgt_list = []
@@ -70,6 +70,25 @@ class Client(client_base.Client):
                 tgt_list.append(d)
         return tgt_list
 
+    def get_fc_target_wwpns(self):
+        """Gets the FC target details."""
+        wwpns = []
+        port_name_list_api = netapp_api.NaElement('fcp-port-name-get-iter')
+        port_name_list_api.add_new_child('max-records', '100')
+        result = self.connection.invoke_successfully(port_name_list_api, True)
+        num_records = result.get_child_content('num-records')
+        if num_records and int(num_records) >= 1:
+            for port_name_info in result.get_child_by_name(
+                    'attributes-list').get_children():
+
+                if port_name_info.get_child_content('is-used') != 'true':
+                    continue
+
+                wwpn = port_name_info.get_child_content('port-name').lower()
+                wwpns.append(wwpn)
+
+        return wwpns
+
     def get_iscsi_service_details(self):
         """Returns iscsi iqn."""
         iscsi_service_iter = netapp_api.NaElement('iscsi-service-get-iter')
@@ -100,7 +119,7 @@ class Client(client_base.Client):
             query = netapp_api.NaElement('query')
             query.add_child_elem(lun_info)
             api.add_child_elem(query)
-            result = self.connection.invoke_successfully(api)
+            result = self.connection.invoke_successfully(api, True)
             if result.get_child_by_name('num-records') and\
                     int(result.get_child_content('num-records')) >= 1:
                 attr_list = result.get_child_by_name('attributes-list')
@@ -139,51 +158,81 @@ class Client(client_base.Client):
                 break
         return map_list
 
-    def get_igroup_by_initiator(self, initiator):
-        """Get igroups by initiator."""
+    def _get_igroup_by_initiator_query(self, initiator, tag):
+        igroup_get_iter = netapp_api.NaElement('igroup-get-iter')
+        igroup_get_iter.add_new_child('max-records', '100')
+        if tag:
+            igroup_get_iter.add_new_child('tag', tag, True)
+
+        query = netapp_api.NaElement('query')
+        igroup_info = netapp_api.NaElement('initiator-group-info')
+        query.add_child_elem(igroup_info)
+        igroup_info.add_new_child('vserver', self.vserver)
+        initiators = netapp_api.NaElement('initiators')
+        igroup_info.add_child_elem(initiators)
+        igroup_get_iter.add_child_elem(query)
+        initiators.add_node_with_children(
+            'initiator-info', **{'initiator-name': initiator})
+
+        # limit results to just the attributes of interest
+        desired_attrs = netapp_api.NaElement('desired-attributes')
+        desired_igroup_info = netapp_api.NaElement('initiator-group-info')
+        desired_igroup_info.add_node_with_children(
+            'initiators', **{'initiator-info': None})
+        desired_igroup_info.add_new_child('vserver', None)
+        desired_igroup_info.add_new_child('initiator-group-name', None)
+        desired_igroup_info.add_new_child('initiator-group-type', None)
+        desired_igroup_info.add_new_child('initiator-group-os-type', None)
+        desired_attrs.add_child_elem(desired_igroup_info)
+        igroup_get_iter.add_child_elem(desired_attrs)
+
+        return igroup_get_iter
+
+    def get_igroup_by_initiators(self, initiator_list):
+        """Get igroups exactly matching a set of initiators."""
         tag = None
         igroup_list = []
+        if not initiator_list:
+            return igroup_list
+
+        initiator_set = set(initiator_list)
+
         while True:
-            igroup_iter = netapp_api.NaElement('igroup-get-iter')
-            igroup_iter.add_new_child('max-records', '100')
-            if tag:
-                igroup_iter.add_new_child('tag', tag, True)
-            query = netapp_api.NaElement('query')
-            igroup_iter.add_child_elem(query)
-            igroup_info = netapp_api.NaElement('initiator-group-info')
-            query.add_child_elem(igroup_info)
-            igroup_info.add_new_child('vserver', self.vserver)
-            initiators = netapp_api.NaElement('initiators')
-            igroup_info.add_child_elem(initiators)
-            initiators.add_node_with_children('initiator-info',
-                                              **{'initiator-name': initiator})
-            des_attrs = netapp_api.NaElement('desired-attributes')
-            des_ig_info = netapp_api.NaElement('initiator-group-info')
-            des_attrs.add_child_elem(des_ig_info)
-            des_ig_info.add_node_with_children('initiators',
-                                               **{'initiator-info': None})
-            des_ig_info.add_new_child('vserver', None)
-            des_ig_info.add_new_child('initiator-group-name', None)
-            des_ig_info.add_new_child('initiator-group-type', None)
-            des_ig_info.add_new_child('initiator-group-os-type', None)
-            igroup_iter.add_child_elem(des_attrs)
-            result = self.connection.invoke_successfully(igroup_iter, False)
+            # C-mode getter APIs can't do an 'and' query, so match the first
+            # initiator (which will greatly narrow the search results) and
+            # filter the rest in this method.
+            query = self._get_igroup_by_initiator_query(initiator_list[0], tag)
+            result = self.connection.invoke_successfully(query, True)
+
             tag = result.get_child_content('next-tag')
-            if result.get_child_content('num-records') and\
-                    int(result.get_child_content('num-records')) > 0:
-                attr_list = result.get_child_by_name('attributes-list')
-                igroups = attr_list.get_children()
-                for igroup in igroups:
-                    ig = dict()
-                    ig['initiator-group-os-type'] = igroup.get_child_content(
-                        'initiator-group-os-type')
-                    ig['initiator-group-type'] = igroup.get_child_content(
-                        'initiator-group-type')
-                    ig['initiator-group-name'] = igroup.get_child_content(
-                        'initiator-group-name')
-                    igroup_list.append(ig)
+            num_records = result.get_child_content('num-records')
+            if num_records and int(num_records) >= 1:
+
+                for igroup_info in result.get_child_by_name(
+                        'attributes-list').get_children():
+
+                    initiator_set_for_igroup = set()
+                    for initiator_info in igroup_info.get_child_by_name(
+                            'initiators').get_children():
+
+                        initiator_set_for_igroup.add(
+                            initiator_info.get_child_content('initiator-name'))
+
+                    if initiator_set == initiator_set_for_igroup:
+                        igroup = {'initiator-group-os-type':
+                                  igroup_info.get_child_content(
+                                      'initiator-group-os-type'),
+                                  'initiator-group-type':
+                                  igroup_info.get_child_content(
+                                      'initiator-group-type'),
+                                  'initiator-group-name':
+                                  igroup_info.get_child_content(
+                                      'initiator-group-name')}
+                        igroup_list.append(igroup)
+
             if tag is None:
                 break
+
         return igroup_list
 
     def clone_lun(self, volume, name, new_name, space_reserved='true',
@@ -240,7 +289,7 @@ class Client(client_base.Client):
         query = netapp_api.NaElement('query')
         lun_iter.add_child_elem(query)
         query.add_node_with_children('lun-info', **args)
-        luns = self.connection.invoke_successfully(lun_iter)
+        luns = self.connection.invoke_successfully(lun_iter, True)
         attr_list = luns.get_child_by_name('attributes-list')
         return attr_list.get_children()
 
diff --git a/cinder/volume/drivers/netapp/dataontap/fc_7mode.py b/cinder/volume/drivers/netapp/dataontap/fc_7mode.py
new file mode 100644 (file)
index 0000000..ba5a6bc
--- /dev/null
@@ -0,0 +1,85 @@
+# Copyright (c) - 2014, Clinton Knight.  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.
+"""
+Volume driver for NetApp Data ONTAP (7-mode) FibreChannel storage systems.
+"""
+
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+from cinder.volume.drivers.netapp.dataontap.block_7mode import \
+    NetAppBlockStorage7modeLibrary as lib_7mode
+from cinder.zonemanager import utils as fczm_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class NetApp7modeFibreChannelDriver(driver.FibreChannelDriver):
+    """NetApp 7-mode FibreChannel volume driver."""
+
+    DRIVER_NAME = 'NetApp_FibreChannel_7mode_direct'
+
+    def __init__(self, *args, **kwargs):
+        super(NetApp7modeFibreChannelDriver, self).__init__(*args, **kwargs)
+        self.library = lib_7mode(self.DRIVER_NAME, 'FC', **kwargs)
+
+    def do_setup(self, context):
+        self.library.do_setup(context)
+
+    def check_for_setup_error(self):
+        self.library.check_for_setup_error()
+
+    def create_volume(self, volume):
+        self.library.create_volume(volume)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        self.library.create_volume_from_snapshot(volume, snapshot)
+
+    def create_cloned_volume(self, volume, src_vref):
+        self.library.create_cloned_volume(volume, src_vref)
+
+    def delete_volume(self, volume):
+        self.library.delete_volume(volume)
+
+    def create_snapshot(self, snapshot):
+        self.library.create_snapshot(snapshot)
+
+    def delete_snapshot(self, snapshot):
+        self.library.delete_snapshot(snapshot)
+
+    def get_volume_stats(self, refresh=False):
+        return self.library.get_volume_stats(refresh)
+
+    def extend_volume(self, volume, new_size):
+        self.library.extend_volume(volume, new_size)
+
+    def ensure_export(self, context, volume):
+        return self.library.ensure_export(context, volume)
+
+    def create_export(self, context, volume):
+        return self.library.create_export(context, volume)
+
+    def remove_export(self, context, volume):
+        self.library.remove_export(context, volume)
+
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector):
+        return self.library.initialize_connection_fc(volume, connector)
+
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        return self.library.terminate_connection_fc(volume, connector,
+                                                    **kwargs)
+
+    def get_pool(self, volume):
+        return self.library.get_pool(volume)
diff --git a/cinder/volume/drivers/netapp/dataontap/fc_cmode.py b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py
new file mode 100644 (file)
index 0000000..fc6fd92
--- /dev/null
@@ -0,0 +1,85 @@
+# Copyright (c) - 2014, Clinton Knight.  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.
+"""
+Volume driver for NetApp Data ONTAP (C-mode) FibreChannel storage systems.
+"""
+
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+from cinder.volume.drivers.netapp.dataontap.block_cmode import \
+    NetAppBlockStorageCmodeLibrary as lib_cmode
+from cinder.zonemanager import utils as fczm_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class NetAppCmodeFibreChannelDriver(driver.FibreChannelDriver):
+    """NetApp C-mode FibreChannel volume driver."""
+
+    DRIVER_NAME = 'NetApp_FibreChannel_Cluster_direct'
+
+    def __init__(self, *args, **kwargs):
+        super(NetAppCmodeFibreChannelDriver, self).__init__(*args, **kwargs)
+        self.library = lib_cmode(self.DRIVER_NAME, 'FC', **kwargs)
+
+    def do_setup(self, context):
+        self.library.do_setup(context)
+
+    def check_for_setup_error(self):
+        self.library.check_for_setup_error()
+
+    def create_volume(self, volume):
+        self.library.create_volume(volume)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        self.library.create_volume_from_snapshot(volume, snapshot)
+
+    def create_cloned_volume(self, volume, src_vref):
+        self.library.create_cloned_volume(volume, src_vref)
+
+    def delete_volume(self, volume):
+        self.library.delete_volume(volume)
+
+    def create_snapshot(self, snapshot):
+        self.library.create_snapshot(snapshot)
+
+    def delete_snapshot(self, snapshot):
+        self.library.delete_snapshot(snapshot)
+
+    def get_volume_stats(self, refresh=False):
+        return self.library.get_volume_stats(refresh)
+
+    def extend_volume(self, volume, new_size):
+        self.library.extend_volume(volume, new_size)
+
+    def ensure_export(self, context, volume):
+        return self.library.ensure_export(context, volume)
+
+    def create_export(self, context, volume):
+        return self.library.create_export(context, volume)
+
+    def remove_export(self, context, volume):
+        self.library.remove_export(context, volume)
+
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector):
+        return self.library.initialize_connection_fc(volume, connector)
+
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        return self.library.terminate_connection_fc(volume, connector,
+                                                    **kwargs)
+
+    def get_pool(self, volume):
+        return self.library.get_pool(volume)
\ No newline at end of file
index 8a701616f60529955bd96da10620f84677fc0730..c66af37a39d564b63923052bbe8a07e3510f3378 100644 (file)
@@ -36,7 +36,8 @@ netapp_proxy_opts = [
     cfg.StrOpt('netapp_storage_protocol',
                default=None,
                help=('The storage protocol to be used on the data path with '
-                     'the storage system; valid values are iscsi or nfs.')), ]
+                     'the storage system; valid values are iscsi, fc, or '
+                     'nfs.')), ]
 
 netapp_connection_opts = [
     cfg.StrOpt('netapp_server_hostname',
@@ -78,8 +79,8 @@ netapp_provisioning_opts = [
     cfg.StrOpt('netapp_volume_list',
                default=None,
                help=('This option is only utilized when the storage protocol '
-                     'is configured to use iSCSI. This option is used to '
-                     'restrict provisioning to the specified controller '
+                     'is configured to use iSCSI or FC. This option is used '
+                     'to restrict provisioning to the specified controller '
                      'volumes. Specify the value of this option to be a '
                      'comma separated list of NetApp controller volume names '
                      'to be used for provisioning.')), ]
@@ -107,7 +108,14 @@ netapp_7mode_opts = [
                      'driver when connecting to an instance with a storage '
                      'family of Data ONTAP operating in 7-Mode. Only use this '
                      'option when utilizing the MultiStore feature on the '
-                     'NetApp storage system.')), ]
+                     'NetApp storage system.')),
+    cfg.StrOpt('netapp_partner_backend_name',
+               default=None,
+               help=('The name of the config.conf stanza for a Data ONTAP '
+                     '(7-mode) HA partner.  This option is only used by the '
+                     'driver when connecting to an instance with a storage '
+                     'family of Data ONTAP operating in 7-Mode, and it is '
+                     'required if the storage protocol selected is FC.')), ]
 
 netapp_img_cache_opts = [
     cfg.IntOpt('thres_avl_size_perc_start',
index 971e16a63e848241d2171e7ae665d0ca5e2b9880..2fc5227250619b589092ba92dbad2e4d2f884c63 100644 (file)
@@ -139,6 +139,12 @@ def log_extra_spec_warnings(extra_specs):
             LOG.warning(msg % args)
 
 
+class hashabledict(dict):
+    """A hashable dictionary that is comparable (i.e. in unit tests, etc.)"""
+    def __hash__(self):
+        return hash(tuple(sorted(self.items())))
+
+
 class OpenStackInfo(object):
     """OS/distribution, release, and version.