1# Copyright 2015 Cloudbase Solutions Srl
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16from oslo_log import log as logging
17import six
18
19from os_win._i18n import _
20from os_win import _utils
21from os_win import constants
22from os_win import exceptions
23from os_win.utils import baseutils
24from os_win.utils import hostutils
25from os_win.utils import pathutils
26from os_win.utils import win32utils
27
28LOG = logging.getLogger(__name__)
29
30
31@six.add_metaclass(baseutils.SynchronizedMeta)
32class ISCSITargetUtils(baseutils.BaseUtils):
33    ID_METHOD_DNS_NAME = 1
34    ID_METHOD_IPV4_ADDR = 2
35    ID_METHOD_MAC_ADDR = 3
36    ID_METHOD_IQN = 4
37    ID_METHOD_IPV6_ADDR = 5
38
39    _ERR_FILE_EXISTS = 80
40
41    def __init__(self):
42        self._conn_wmi = self._get_wmi_conn('//./root/wmi')
43        self._ensure_wt_provider_available()
44
45        self._pathutils = pathutils.PathUtils()
46        self._hostutils = hostutils.HostUtils()
47        self._win32utils = win32utils.Win32Utils()
48
49        self._win_gteq_6_3 = self._hostutils.check_min_windows_version(6, 3)
50
51    def _ensure_wt_provider_available(self):
52        try:
53            self._conn_wmi.WT_Portal
54        except AttributeError:
55            err_msg = _("The Windows iSCSI target provider is not available.")
56            raise exceptions.ISCSITargetException(err_msg)
57
58    def get_supported_disk_format(self):
59        return (constants.DISK_FORMAT_VHDX
60                if self._win_gteq_6_3 else constants.DISK_FORMAT_VHD)
61
62    def get_supported_vhd_type(self):
63        return (constants.VHD_TYPE_DYNAMIC
64                if self._win_gteq_6_3 else constants.VHD_TYPE_FIXED)
65
66    def get_portal_locations(self, available_only=True,
67                             fail_if_none_found=True):
68        wt_portals = self._conn_wmi.WT_Portal()
69
70        if available_only:
71            wt_portals = list(filter(lambda portal: portal.Listen, wt_portals))
72
73        if not wt_portals and fail_if_none_found:
74            err_msg = _("No valid iSCSI portal was found.")
75            raise exceptions.ISCSITargetException(err_msg)
76
77        portal_locations = [self._get_portal_location(portal)
78                            for portal in wt_portals]
79        return portal_locations
80
81    def _get_portal_location(self, wt_portal):
82        return '%s:%s' % (wt_portal.Address, wt_portal.Port)
83
84    def _get_wt_host(self, target_name, fail_if_not_found=True):
85        hosts = self._conn_wmi.WT_Host(HostName=target_name)
86
87        if hosts:
88            return hosts[0]
89        elif fail_if_not_found:
90            err_msg = _('Could not find iSCSI target %s')
91            raise exceptions.ISCSITargetException(err_msg % target_name)
92
93    def _get_wt_disk(self, description, fail_if_not_found=True):
94        # We can retrieve WT Disks only by description.
95        wt_disks = self._conn_wmi.WT_Disk(Description=description)
96        if wt_disks:
97            return wt_disks[0]
98        elif fail_if_not_found:
99            err_msg = _('Could not find WT Disk: %s')
100            raise exceptions.ISCSITargetException(err_msg % description)
101
102    def _get_wt_snapshot(self, description, fail_if_not_found=True):
103        wt_snapshots = self._conn_wmi.WT_Snapshot(Description=description)
104        if wt_snapshots:
105            return wt_snapshots[0]
106        elif fail_if_not_found:
107            err_msg = _('Could not find WT Snapshot: %s')
108            raise exceptions.ISCSITargetException(err_msg % description)
109
110    def _get_wt_idmethod(self, initiator, target_name):
111        wt_idmethod = self._conn_wmi.WT_IDMethod(HostName=target_name,
112                                                 Value=initiator)
113        if wt_idmethod:
114            return wt_idmethod[0]
115
116    def create_iscsi_target(self, target_name, fail_if_exists=False):
117        """Creates ISCSI target."""
118        try:
119            self._conn_wmi.WT_Host.NewHost(HostName=target_name)
120        except exceptions.x_wmi as wmi_exc:
121            err_code = _utils.get_com_error_code(wmi_exc.com_error)
122            target_exists = err_code == self._ERR_FILE_EXISTS
123
124            if not target_exists or fail_if_exists:
125                err_msg = _('Failed to create iSCSI target: %s.')
126                raise exceptions.ISCSITargetWMIException(err_msg % target_name,
127                                                         wmi_exc=wmi_exc)
128            else:
129                LOG.info('The iSCSI target %s already exists.',
130                         target_name)
131
132    def delete_iscsi_target(self, target_name):
133        """Removes ISCSI target."""
134        try:
135            wt_host = self._get_wt_host(target_name, fail_if_not_found=False)
136            if not wt_host:
137                LOG.debug('Skipping deleting target %s as it does not '
138                          'exist.', target_name)
139                return
140            wt_host.RemoveAllWTDisks()
141            wt_host.Delete_()
142        except exceptions.x_wmi as wmi_exc:
143            err_msg = _("Failed to delete ISCSI target %s")
144            raise exceptions.ISCSITargetWMIException(err_msg % target_name,
145                                                     wmi_exc=wmi_exc)
146
147    def iscsi_target_exists(self, target_name):
148        wt_host = self._get_wt_host(target_name, fail_if_not_found=False)
149        return wt_host is not None
150
151    def get_target_information(self, target_name):
152        wt_host = self._get_wt_host(target_name)
153
154        info = {}
155        info['target_iqn'] = wt_host.TargetIQN
156        info['enabled'] = wt_host.Enabled
157        info['connected'] = bool(wt_host.Status)
158
159        # Note(lpetrut): Cinder uses only one-way CHAP authentication.
160        if wt_host.EnableCHAP:
161            info['auth_method'] = 'CHAP'
162            info['auth_username'] = wt_host.CHAPUserName
163            info['auth_password'] = wt_host.CHAPSecret
164
165        return info
166
167    def set_chap_credentials(self, target_name, chap_username, chap_password):
168        try:
169            wt_host = self._get_wt_host(target_name)
170            wt_host.EnableCHAP = True
171            wt_host.CHAPUserName = chap_username
172            wt_host.CHAPSecret = chap_password
173            wt_host.put()
174        except exceptions.x_wmi as wmi_exc:
175            err_msg = _('Failed to set CHAP credentials on target %s.')
176            raise exceptions.ISCSITargetWMIException(err_msg % target_name,
177                                                     wmi_exc=wmi_exc)
178
179    def associate_initiator_with_iscsi_target(self, initiator,
180                                              target_name,
181                                              id_method=ID_METHOD_IQN):
182        wt_idmethod = self._get_wt_idmethod(initiator, target_name)
183        if wt_idmethod:
184            return
185
186        try:
187            wt_idmethod = self._conn_wmi.WT_IDMethod.new()
188            wt_idmethod.HostName = target_name
189            wt_idmethod.Method = id_method
190            wt_idmethod.Value = initiator
191            wt_idmethod.put()
192        except exceptions.x_wmi as wmi_exc:
193            err_msg = _('Could not associate initiator %(initiator)s to '
194                        'iSCSI target: %(target_name)s.')
195            raise exceptions.ISCSITargetWMIException(
196                err_msg % dict(initiator=initiator,
197                               target_name=target_name),
198                wmi_exc=wmi_exc)
199
200    def deassociate_initiator(self, initiator, target_name):
201        try:
202            wt_idmethod = self._get_wt_idmethod(initiator, target_name)
203            if wt_idmethod:
204                wt_idmethod.Delete_()
205        except exceptions.x_wmi as wmi_exc:
206            err_msg = _('Could not deassociate initiator %(initiator)s from '
207                        'iSCSI target: %(target_name)s.')
208            raise exceptions.ISCSITargetWMIException(
209                err_msg % dict(initiator=initiator,
210                               target_name=target_name),
211                wmi_exc=wmi_exc)
212
213    def create_wt_disk(self, vhd_path, wtd_name, size_mb=None):
214        try:
215            self._conn_wmi.WT_Disk.NewWTDisk(DevicePath=vhd_path,
216                                             Description=wtd_name,
217                                             SizeInMB=size_mb)
218        except exceptions.x_wmi as wmi_exc:
219            err_msg = _('Failed to create WT Disk. '
220                        'VHD path: %(vhd_path)s '
221                        'WT disk name: %(wtd_name)s')
222            raise exceptions.ISCSITargetWMIException(
223                err_msg % dict(vhd_path=vhd_path,
224                               wtd_name=wtd_name),
225                wmi_exc=wmi_exc)
226
227    def import_wt_disk(self, vhd_path, wtd_name):
228        """Import a vhd/x image to be used by Windows iSCSI targets."""
229        try:
230            self._conn_wmi.WT_Disk.ImportWTDisk(DevicePath=vhd_path,
231                                                Description=wtd_name)
232        except exceptions.x_wmi as wmi_exc:
233            err_msg = _("Failed to import WT disk: %s.")
234            raise exceptions.ISCSITargetWMIException(err_msg % vhd_path,
235                                                     wmi_exc=wmi_exc)
236
237    def change_wt_disk_status(self, wtd_name, enabled):
238        try:
239            wt_disk = self._get_wt_disk(wtd_name)
240            wt_disk.Enabled = enabled
241            wt_disk.put()
242        except exceptions.x_wmi as wmi_exc:
243            err_msg = _('Could not change disk status. WT Disk name: %s')
244            raise exceptions.ISCSITargetWMIException(err_msg % wtd_name,
245                                                     wmi_exc=wmi_exc)
246
247    def remove_wt_disk(self, wtd_name):
248        try:
249            wt_disk = self._get_wt_disk(wtd_name, fail_if_not_found=False)
250            if wt_disk:
251                wt_disk.Delete_()
252        except exceptions.x_wmi as wmi_exc:
253            err_msg = _("Failed to remove WT disk: %s.")
254            raise exceptions.ISCSITargetWMIException(err_msg % wtd_name,
255                                                     wmi_exc=wmi_exc)
256
257    def extend_wt_disk(self, wtd_name, additional_mb):
258        try:
259            wt_disk = self._get_wt_disk(wtd_name)
260            wt_disk.Extend(additional_mb)
261        except exceptions.x_wmi as wmi_exc:
262            err_msg = _('Could not extend WT Disk %(wtd_name)s '
263                        'with additional %(additional_mb)s MB.')
264            raise exceptions.ISCSITargetWMIException(
265                err_msg % dict(wtd_name=wtd_name,
266                               additional_mb=additional_mb),
267                wmi_exc=wmi_exc)
268
269    def add_disk_to_target(self, wtd_name, target_name):
270        """Adds the disk to the target."""
271        try:
272            wt_disk = self._get_wt_disk(wtd_name)
273            wt_host = self._get_wt_host(target_name)
274            wt_host.AddWTDisk(wt_disk.WTD)
275        except exceptions.x_wmi as wmi_exc:
276            err_msg = _('Could not add WTD Disk %(wtd_name)s to '
277                        'iSCSI target %(target_name)s.')
278            raise exceptions.ISCSITargetWMIException(
279                err_msg % dict(wtd_name=wtd_name,
280                               target_name=target_name),
281                wmi_exc=wmi_exc)
282
283    def create_snapshot(self, wtd_name, snapshot_name):
284        """Driver entry point for creating a snapshot."""
285        try:
286            wt_disk = self._get_wt_disk(wtd_name)
287            snap_id = self._conn_wmi.WT_Snapshot.Create(WTD=wt_disk.WTD)[0]
288
289            wt_snap = self._conn_wmi.WT_Snapshot(Id=snap_id)[0]
290            wt_snap.Description = snapshot_name
291            wt_snap.put()
292        except exceptions.x_wmi as wmi_exc:
293            err_msg = _('Failed to create snapshot. '
294                        'WT Disk name: %(wtd_name)s '
295                        'Snapshot name: %(snapshot_name)s')
296            raise exceptions.ISCSITargetWMIException(
297                err_msg % dict(wtd_name=wtd_name,
298                               snapshot_name=snapshot_name),
299                wmi_exc=wmi_exc)
300
301    def export_snapshot(self, snapshot_name, dest_path):
302        """Driver entry point for exporting snapshots as volumes."""
303        try:
304            wt_snap = self._get_wt_snapshot(snapshot_name)
305            wt_disk_id = wt_snap.Export()[0]
306            # This export is a read-only shadow copy, needing to be copied
307            # to another disk.
308            wt_disk = self._conn_wmi.WT_Disk(WTD=wt_disk_id)[0]
309            wt_disk.Description = '%s-%s-temp' % (snapshot_name, wt_disk_id)
310            wt_disk.put()
311            src_path = wt_disk.DevicePath
312
313            self._pathutils.copy(src_path, dest_path)
314
315            wt_disk.Delete_()
316        except exceptions.x_wmi as wmi_exc:
317            err_msg = _('Failed to export snapshot %(snapshot_name)s '
318                        'to %(dest_path)s.')
319            raise exceptions.ISCSITargetWMIException(
320                err_msg % dict(snapshot_name=snapshot_name,
321                               dest_path=dest_path),
322                wmi_exc=wmi_exc)
323
324    def delete_snapshot(self, snapshot_name):
325        """Driver entry point for deleting a snapshot."""
326        try:
327            wt_snapshot = self._get_wt_snapshot(snapshot_name,
328                                                fail_if_not_found=False)
329            if wt_snapshot:
330                wt_snapshot.Delete_()
331        except exceptions.x_wmi as wmi_exc:
332            err_msg = _('Failed delete snapshot %s.')
333            raise exceptions.ISCSITargetWMIException(err_msg % snapshot_name,
334                                                     wmi_exc=wmi_exc)
335