Browse Source

3PAR: Create volume from snapshot with larger size

Refactored the migrate_volume code to be usable by both migrate volume
and create volume from snapshot with a larger size. The common functionality
is to clone a volume as a base volume. Migrate volume specifies a different
CPG, whereas create volume from snapshot uses the same CPG.

Change-Id: I4081807294d918fc0e9c2e17bae89b6df7ee1513
Closes-Bug: #1279478
tags/2014.1.b3
Ramy Asselin 5 years ago
parent
commit
9090f99986
2 changed files with 151 additions and 56 deletions
  1. 67
    0
      cinder/tests/test_hp3par.py
  2. 84
    56
      cinder/volume/drivers/san/hp/hp_3par_common.py

+ 67
- 0
cinder/tests/test_hp3par.py View File

@@ -591,6 +591,73 @@ class HP3PARBaseDriver(object):
591 591
                           self.driver.create_volume_from_snapshot,
592 592
                           volume, self.snapshot)
593 593
 
594
+    def test_create_volume_from_snapshot_and_extend(self):
595
+        # setup_mock_client drive with default configuration
596
+        # and return the mock HTTP 3PAR client
597
+        conf = {
598
+            'getPorts.return_value': {
599
+                'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]},
600
+            'getTask.return_value': {
601
+                'status': 1},
602
+            'copyVolume.return_value': {'taskid': 1},
603
+            'getVolume.return_value': {}
604
+        }
605
+
606
+        mock_client = self.setup_driver(mock_conf=conf)
607
+
608
+        volume = self.volume.copy()
609
+        volume['size'] = self.volume['size'] + 10
610
+        self.driver.create_volume_from_snapshot(volume, self.snapshot)
611
+
612
+        comment = (
613
+            '{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",'
614
+            ' "display_name": "Foo Volume",'
615
+            ' "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"}')
616
+
617
+        volume_name_3par = self.driver.common._encode_name(volume['id'])
618
+        osv_matcher = 'osv-' + volume_name_3par
619
+        omv_matcher = 'omv-' + volume_name_3par
620
+
621
+        expected = [
622
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
623
+            mock.call.createSnapshot(
624
+                self.VOLUME_3PAR_NAME,
625
+                'oss-L4I73ONuTci9Fd4ceij-MQ',
626
+                {
627
+                    'comment': comment,
628
+                    'readOnly': False}),
629
+            mock.call.copyVolume(osv_matcher, omv_matcher, mock.ANY, mock.ANY),
630
+            mock.call.getTask(mock.ANY),
631
+            mock.call.getVolume(osv_matcher),
632
+            mock.call.deleteVolume(osv_matcher),
633
+            mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}),
634
+            mock.call.growVolume(osv_matcher, 10 * 1024),
635
+            mock.call.logout()]
636
+
637
+        mock_client.assert_has_calls(expected)
638
+
639
+    def test_create_volume_from_snapshot_and_extend_copy_fail(self):
640
+        # setup_mock_client drive with default configuration
641
+        # and return the mock HTTP 3PAR client
642
+        conf = {
643
+            'getPorts.return_value': {
644
+                'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]},
645
+            'getTask.return_value': {
646
+                'status': 4,
647
+                'failure message': 'out of disk space'},
648
+            'copyVolume.return_value': {'taskid': 1},
649
+            'getVolume.return_value': {}
650
+        }
651
+
652
+        mock_client = self.setup_driver(mock_conf=conf)
653
+
654
+        volume = self.volume.copy()
655
+        volume['size'] = self.volume['size'] + 10
656
+
657
+        self.assertRaises(exception.CinderException,
658
+                          self.driver.create_volume_from_snapshot,
659
+                          volume, self.snapshot)
660
+
594 661
     @mock.patch.object(volume_types, 'get_volume_type')
595 662
     def test_create_volume_from_snapshot_qos(self, _mock_volume_types):
596 663
         # setup_mock_client drive with default configuration

+ 84
- 56
cinder/volume/drivers/san/hp/hp_3par_common.py View File

@@ -117,10 +117,11 @@ class HP3PARCommon(object):
117 117
         2.0.1 - Updated to use qos_specs, added new qos settings and personas
118 118
         2.0.2 - Add back-end assisted volume migrate
119 119
         2.0.3 - Allow deleting missing snapshots bug #1283233
120
+        2.0.4 - Allow volumes created from snapshots to be larger bug #1279478
120 121
 
121 122
     """
122 123
 
123
-    VERSION = "2.0.3"
124
+    VERSION = "2.0.4"
124 125
 
125 126
     stats = {}
126 127
 
@@ -735,7 +736,7 @@ class HP3PARCommon(object):
735 736
                 return status
736 737
             time.sleep(poll_interval_sec)
737 738
 
738
-    def _copy_volume(self, src_name, dest_name, cpg=None, snap_cpg=None,
739
+    def _copy_volume(self, src_name, dest_name, cpg, snap_cpg=None,
739 740
                      tpvv=True):
740 741
         # Virtual volume sets are not supported with the -online option
741 742
         LOG.debug(_('Creating clone of a volume %(src)s to %(dest)s.') %
@@ -848,15 +849,14 @@ class HP3PARCommon(object):
848 849
     def create_volume_from_snapshot(self, volume, snapshot):
849 850
         """Creates a volume from a snapshot.
850 851
 
851
-        TODO: support using the size from the user.
852 852
         """
853 853
         LOG.debug("Create Volume from Snapshot\n%s\n%s" %
854 854
                   (pprint.pformat(volume['display_name']),
855 855
                    pprint.pformat(snapshot['display_name'])))
856 856
 
857
-        if snapshot['volume_size'] != volume['size']:
858
-            err = "You cannot change size of the volume.  It must "
859
-            "be the same as the snapshot."
857
+        if volume['size'] < snapshot['volume_size']:
858
+            err = ("You cannot reduce size of the volume.  It must "
859
+                   "be greater than or equal to the snapshot.")
860 860
             LOG.error(err)
861 861
             raise exception.InvalidInput(reason=err)
862 862
 
@@ -891,6 +891,25 @@ class HP3PARCommon(object):
891 891
                         'readOnly': False}
892 892
 
893 893
             self.client.createSnapshot(volume_name, snap_name, optional)
894
+
895
+            # Grow the snapshot if needed
896
+            growth_size = volume['size'] - snapshot['volume_size']
897
+            if growth_size > 0:
898
+                try:
899
+                    LOG.debug(_('Converting to base volume type: %s.') %
900
+                              volume['id'])
901
+                    self._convert_to_base_volume(volume)
902
+                    growth_size_mib = growth_size * units.GiB / units.MiB
903
+                    LOG.debug(_('Growing volume: %(id)s by %(size)s GiB.') %
904
+                              {'id': volume['id'], 'size': growth_size})
905
+                    self.client.growVolume(volume_name, growth_size_mib)
906
+                except Exception as ex:
907
+                    LOG.error(_("Error extending volume %(id)s. Ex: %(ex)s") %
908
+                              {'id': volume['id'], 'ex': str(ex)})
909
+                    # Delete the volume if unable to grow it
910
+                    self.client.deleteVolume(volume_name)
911
+                    raise exception.CinderException(ex)
912
+
894 913
             if qos or vvs_name is not None:
895 914
                 cpg = self._get_key_value(hp3par_keys, 'cpg',
896 915
                                           self.config.hp3par_cpg)
@@ -1020,45 +1039,59 @@ class HP3PARCommon(object):
1020 1039
         dbg = {'id': volume['id'], 'host': host['host']}
1021 1040
         LOG.debug(_('enter: migrate_volume: id=%(id)s, host=%(host)s.') % dbg)
1022 1041
 
1042
+        false_ret = (False, None)
1043
+
1044
+        # Make sure volume is not attached
1045
+        if volume['status'] != 'available':
1046
+            LOG.debug(_('Volume is attached: migrate_volume: '
1047
+                        'id=%(id)s, host=%(host)s.') % dbg)
1048
+            return false_ret
1049
+
1050
+        if 'location_info' not in host['capabilities']:
1051
+            return false_ret
1052
+
1053
+        info = host['capabilities']['location_info']
1023 1054
         try:
1024
-            false_ret = (False, None)
1055
+            (dest_type, dest_id, dest_cpg) = info.split(':')
1056
+        except ValueError:
1057
+            return false_ret
1025 1058
 
1026
-            # Make sure volume is not attached
1027
-            if volume['status'] != 'available':
1028
-                LOG.debug(_('Volume is attached: migrate_volume: '
1029
-                            'id=%(id)s, host=%(host)s.') % dbg)
1030
-                return false_ret
1059
+        sys_info = self.client.getStorageSystemInfo()
1060
+        if not (dest_type == 'HP3PARDriver' and
1061
+                dest_id == sys_info['serialNumber']):
1062
+            LOG.debug(_('Dest does not match: migrate_volume: '
1063
+                        'id=%(id)s, host=%(host)s.') % dbg)
1064
+            return false_ret
1031 1065
 
1032
-            if 'location_info' not in host['capabilities']:
1033
-                return false_ret
1066
+        type_info = self.get_volume_settings_from_type(volume)
1034 1067
 
1035
-            info = host['capabilities']['location_info']
1036
-            try:
1037
-                (dest_type, dest_id, dest_cpg) = info.split(':')
1038
-            except ValueError:
1039
-                return false_ret
1068
+        if dest_cpg == type_info['cpg']:
1069
+            LOG.debug(_('CPGs are the same: migrate_volume: '
1070
+                        'id=%(id)s, host=%(host)s.') % dbg)
1071
+            return false_ret
1040 1072
 
1041
-            sys_info = self.client.getStorageSystemInfo()
1042
-            if not (dest_type == 'HP3PARDriver' and
1043
-                    dest_id == sys_info['serialNumber']):
1044
-                LOG.debug(_('Dest does not match: migrate_volume: '
1045
-                            'id=%(id)s, host=%(host)s.') % dbg)
1046
-                return false_ret
1073
+        # Check to make sure CPGs are in the same domain
1074
+        src_domain = self.get_domain(type_info['cpg'])
1075
+        dst_domain = self.get_domain(dest_cpg)
1076
+        if src_domain != dst_domain:
1077
+            LOG.debug(_('CPGs in different domains: migrate_volume: '
1078
+                        'id=%(id)s, host=%(host)s.') % dbg)
1079
+            return false_ret
1047 1080
 
1048
-            type_info = self.get_volume_settings_from_type(volume)
1081
+        self._convert_to_base_volume(volume, new_cpg=dest_cpg)
1049 1082
 
1050
-            if dest_cpg == type_info['cpg']:
1051
-                LOG.debug(_('CPGs are the same: migrate_volume: '
1052
-                            'id=%(id)s, host=%(host)s.') % dbg)
1053
-                return false_ret
1083
+        # TODO(Ramy) When volume retype is available,
1084
+        # use that to change the type
1085
+        LOG.debug(_('leave: migrate_volume: id=%(id)s, host=%(host)s.') % dbg)
1086
+        return (True, None)
1054 1087
 
1055
-            # Check to make sure CPGs are in the same domain
1056
-            src_domain = self.get_domain(type_info['cpg'])
1057
-            dst_domain = self.get_domain(dest_cpg)
1058
-            if src_domain != dst_domain:
1059
-                LOG.debug(_('CPGs in different domains: migrate_volume: '
1060
-                            'id=%(id)s, host=%(host)s.') % dbg)
1061
-                return false_ret
1088
+    def _convert_to_base_volume(self, volume, new_cpg=None):
1089
+        try:
1090
+            type_info = self.get_volume_settings_from_type(volume)
1091
+            if new_cpg:
1092
+                cpg = new_cpg
1093
+            else:
1094
+                cpg = type_info['cpg']
1062 1095
 
1063 1096
             # Change the name such that it is unique since 3PAR
1064 1097
             # names must be unique across all CPGs
@@ -1067,40 +1100,38 @@ class HP3PARCommon(object):
1067 1100
 
1068 1101
             # Create a physical copy of the volume
1069 1102
             task_id = self._copy_volume(volume_name, temp_vol_name,
1070
-                                        dest_cpg, dest_cpg, type_info['tpvv'])
1103
+                                        cpg, cpg, type_info['tpvv'])
1071 1104
 
1072
-            LOG.debug(_('Copy volume scheduled: migrate_volume: '
1073
-                        'id=%(id)s, host=%(host)s.') % dbg)
1105
+            LOG.debug(_('Copy volume scheduled: convert_to_base_volume: '
1106
+                        'id=%s.') % volume['id'])
1074 1107
 
1075 1108
             # Wait for the physical copy task to complete
1076 1109
             status = self._wait_for_task(task_id)
1077 1110
             if status['status'] is not self.client.TASK_DONE:
1078
-                dbg['status'] = status
1079
-                msg = _('Copy volume task failed: migrate_volume: '
1080
-                        'id=%(id)s, host=%(host)s, status=%(status)s.') % dbg
1111
+                dbg = {'status': status, 'id': volume['id']}
1112
+                msg = _('Copy volume task failed: convert_to_base_volume: '
1113
+                        'id=%(id)s, status=%(status)s.') % dbg
1081 1114
                 raise exception.CinderException(msg)
1082 1115
             else:
1083
-                LOG.debug(_('Copy volume completed: migrate_volume: '
1084
-                            'id=%(id)s, host=%(host)s.') % dbg)
1116
+                LOG.debug(_('Copy volume completed: convert_to_base_volume: '
1117
+                            'id=%s.') % volume['id'])
1085 1118
 
1086 1119
             comment = self._get_3par_vol_comment(volume_name)
1087 1120
             if comment:
1088 1121
                 self.client.modifyVolume(temp_vol_name, {'comment': comment})
1089
-            LOG.debug(_('Migrated volume rename completed: migrate_volume: '
1090
-                        'id=%(id)s, host=%(host)s.') % dbg)
1122
+            LOG.debug(_('Volume rename completed: convert_to_base_volume: '
1123
+                        'id=%s.') % volume['id'])
1091 1124
 
1092 1125
             # Delete source volume after the copy is complete
1093 1126
             self.client.deleteVolume(volume_name)
1094
-            LOG.debug(_('Delete src volume completed: migrate_volume: '
1095
-                        'id=%(id)s, host=%(host)s.') % dbg)
1127
+            LOG.debug(_('Delete src volume completed: convert_to_base_volume: '
1128
+                        'id=%s.') % volume['id'])
1096 1129
 
1097 1130
             # Rename the new volume to the original name
1098 1131
             self.client.modifyVolume(temp_vol_name, {'newName': volume_name})
1099 1132
 
1100
-            # TODO(Ramy) When volume retype is available,
1101
-            # use that to change the type
1102
-            LOG.info(_('Completed: migrate_volume: '
1103
-                       'id=%(id)s, host=%(host)s.') % dbg)
1133
+            LOG.info(_('Completed: convert_to_base_volume: '
1134
+                       'id=%s.') % volume['id'])
1104 1135
         except hpexceptions.HTTPConflict:
1105 1136
             msg = _("Volume (%s) already exists on array.") % volume_name
1106 1137
             LOG.error(msg)
@@ -1118,9 +1149,6 @@ class HP3PARCommon(object):
1118 1149
             LOG.error(str(ex))
1119 1150
             raise exception.CinderException(ex)
1120 1151
 
1121
-        LOG.debug(_('leave: migrate_volume: id=%(id)s, host=%(host)s.') % dbg)
1122
-        return (True, None)
1123
-
1124 1152
     def delete_snapshot(self, snapshot):
1125 1153
         LOG.debug("Delete Snapshot id %s %s" % (snapshot['id'],
1126 1154
                                                 pprint.pformat(snapshot)))

Loading…
Cancel
Save