Shared filesystem management project for OpenStack.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

qnap.py 40KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. # Copyright (c) 2016 QNAP Systems, Inc.
  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. """
  16. Share driver for QNAP Storage.
  17. This driver supports QNAP Storage for NFS.
  18. """
  19. import datetime
  20. import math
  21. import re
  22. import time
  23. from oslo_config import cfg
  24. from oslo_log import log as logging
  25. from oslo_utils import timeutils
  26. from oslo_utils import units
  27. from manila.common import constants
  28. from manila import exception
  29. from manila.i18n import _
  30. from manila import share
  31. from manila.share import driver
  32. from manila.share.drivers.qnap import api
  33. from manila.share import share_types
  34. from manila import utils
  35. LOG = logging.getLogger(__name__)
  36. qnap_manila_opts = [
  37. cfg.StrOpt('qnap_management_url',
  38. required=True,
  39. help='The URL to manage QNAP Storage.'),
  40. cfg.HostAddressOpt('qnap_share_ip',
  41. required=True,
  42. help='NAS share IP for mounting shares.'),
  43. cfg.StrOpt('qnap_nas_login',
  44. required=True,
  45. help='Username for QNAP storage.'),
  46. cfg.StrOpt('qnap_nas_password',
  47. required=True,
  48. secret=True,
  49. help='Password for QNAP storage.'),
  50. cfg.StrOpt('qnap_poolname',
  51. required=True,
  52. help='Pool within which QNAP shares must be created.'),
  53. ]
  54. CONF = cfg.CONF
  55. CONF.register_opts(qnap_manila_opts)
  56. class QnapShareDriver(driver.ShareDriver):
  57. """OpenStack driver to enable QNAP Storage.
  58. Version history:
  59. 1.0.0 - Initial driver (Only NFS)
  60. 1.0.1 - Add support for QES fw 1.1.4.
  61. 1.0.2 - Fix bug #1736370, QNAP Manila driver: Access rule setting is
  62. override by the another access rule.
  63. 1.0.3 - Add supports for Thin Provisioning, SSD Cache, Deduplication
  64. and Compression.
  65. 1.0.4 - Add support for QES fw 2.0.0.
  66. 1.0.5 - Fix bug #1773761, when user tries to manage share, the size
  67. of managed share should not be changed.
  68. 1.0.6 - Add support for QES fw 2.1.0.
  69. 1.0.7 - Add support for QES fw on TDS series NAS model.
  70. 1.0.8 - Fix bug, driver should not manage snapshot which does not
  71. exist in NAS.
  72. """
  73. DRIVER_VERSION = '1.0.8'
  74. def __init__(self, *args, **kwargs):
  75. """Initialize QnapShareDriver."""
  76. super(QnapShareDriver, self).__init__(False, *args, **kwargs)
  77. self.private_storage = kwargs.get('private_storage')
  78. self.api_executor = None
  79. self.group_stats = {}
  80. self.configuration.append_config_values(qnap_manila_opts)
  81. self.share_api = share.API()
  82. def do_setup(self, context):
  83. """Setup the QNAP Manila share driver."""
  84. self.ctxt = context
  85. LOG.debug('context: %s', context)
  86. # Setup API Executor
  87. try:
  88. self.api_executor = self._create_api_executor()
  89. except Exception:
  90. LOG.exception('Failed to create HTTP client. Check IP '
  91. 'address, port, username, password and make '
  92. 'sure the array version is compatible.')
  93. raise
  94. def check_for_setup_error(self):
  95. """Check the status of setup."""
  96. if self.api_executor is None:
  97. msg = _("Failed to instantiate API client to communicate with "
  98. "QNAP storage systems.")
  99. raise exception.ShareBackendException(msg=msg)
  100. def _create_api_executor(self):
  101. """Create API executor by NAS model."""
  102. """LOG.debug('CONF.qnap_nas_login=%(conf)s',
  103. {'conf': CONF.qnap_nas_login})
  104. LOG.debug('self.configuration.qnap_nas_login=%(conf)s',
  105. {'conf': self.configuration.qnap_nas_login})"""
  106. self.api_executor = api.QnapAPIExecutor(
  107. username=self.configuration.qnap_nas_login,
  108. password=self.configuration.qnap_nas_password,
  109. management_url=self.configuration.qnap_management_url)
  110. display_model_name, internal_model_name, fw_version = (
  111. self.api_executor.get_basic_info(
  112. self.configuration.qnap_management_url))
  113. pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)")
  114. matches = pattern.match(display_model_name)
  115. if not matches:
  116. return None
  117. model_type = matches.group(1)
  118. ts_model_types = (
  119. "TS", "SS", "IS", "TVS", "TBS"
  120. )
  121. tes_model_types = (
  122. "TES", "TDS"
  123. )
  124. es_model_types = (
  125. "ES",
  126. )
  127. if model_type in ts_model_types:
  128. if (fw_version.startswith("4.2") or fw_version.startswith("4.3")):
  129. LOG.debug('Create TS API Executor')
  130. # modify the pool name to pool index
  131. self.configuration.qnap_poolname = (
  132. self._get_ts_model_pool_id(
  133. self.configuration.qnap_poolname))
  134. return api.QnapAPIExecutorTS(
  135. username=self.configuration.qnap_nas_login,
  136. password=self.configuration.qnap_nas_password,
  137. management_url=self.configuration.qnap_management_url)
  138. elif model_type in tes_model_types:
  139. if 'TS' in internal_model_name:
  140. if (fw_version.startswith("4.2") or
  141. fw_version.startswith("4.3")):
  142. LOG.debug('Create TS API Executor')
  143. # modify the pool name to pool index
  144. self.configuration.qnap_poolname = (
  145. self._get_ts_model_pool_id(
  146. self.configuration.qnap_poolname))
  147. return api.QnapAPIExecutorTS(
  148. username=self.configuration.qnap_nas_login,
  149. password=self.configuration.qnap_nas_password,
  150. management_url=self.configuration.qnap_management_url)
  151. elif "1.1.2" <= fw_version <= "2.1.9999":
  152. LOG.debug('Create ES API Executor')
  153. return api.QnapAPIExecutor(
  154. username=self.configuration.qnap_nas_login,
  155. password=self.configuration.qnap_nas_password,
  156. management_url=self.configuration.qnap_management_url)
  157. elif model_type in es_model_types:
  158. if "1.1.2" <= fw_version <= "2.1.9999":
  159. LOG.debug('Create ES API Executor')
  160. return api.QnapAPIExecutor(
  161. username=self.configuration.qnap_nas_login,
  162. password=self.configuration.qnap_nas_password,
  163. management_url=self.configuration.qnap_management_url)
  164. msg = _('QNAP Storage model is not supported by this driver.')
  165. raise exception.ShareBackendException(msg=msg)
  166. def _get_ts_model_pool_id(self, pool_name):
  167. """Modify the pool name to pool index."""
  168. pattern = re.compile(r"^(\d+)+|^Storage Pool (\d+)+")
  169. matches = pattern.match(pool_name)
  170. if matches.group(1):
  171. return matches.group(1)
  172. else:
  173. return matches.group(2)
  174. @utils.synchronized('qnap-gen_name')
  175. def _gen_random_name(self, type):
  176. if type == 'share':
  177. infix = "shr-"
  178. elif type == 'snapshot':
  179. infix = "snp-"
  180. elif type == 'host':
  181. infix = "hst-"
  182. else:
  183. infix = ""
  184. return ("manila-%(ifx)s%(time)s" %
  185. {'ifx': infix,
  186. 'time': timeutils.utcnow().strftime('%Y%m%d%H%M%S%f')})
  187. def _gen_host_name(self, vol_name_timestamp, access_level):
  188. # host_name will be manila-{vol_name_timestamp}-ro or
  189. # manila-{vol_name_timestamp}-rw
  190. return 'manila-{}-{}'.format(vol_name_timestamp, access_level)
  191. def _get_timestamp_from_vol_name(self, vol_name):
  192. vol_name_split = vol_name.split('-')
  193. dt = datetime.datetime.strptime(vol_name_split[2], '%Y%m%d%H%M%S%f')
  194. return int(time.mktime(dt.timetuple()))
  195. def _get_location_path(self, share_name, share_proto, ip, vol_id):
  196. if share_proto == 'NFS':
  197. vol = self.api_executor.get_specific_volinfo(vol_id)
  198. vol_mount_path = vol.find('vol_mount_path').text
  199. location = '%s:%s' % (ip, vol_mount_path)
  200. else:
  201. msg = _('Invalid NAS protocol: %s') % share_proto
  202. raise exception.InvalidInput(reason=msg)
  203. export_location = {
  204. 'path': location,
  205. 'is_admin_only': False,
  206. }
  207. return export_location
  208. def _update_share_stats(self):
  209. """Get latest share stats."""
  210. backend_name = (self.configuration.safe_get(
  211. 'share_backend_name') or
  212. self.__class__.__name__)
  213. LOG.debug('backend_name=%(backend_name)s',
  214. {'backend_name': backend_name})
  215. selected_pool = self.api_executor.get_specific_poolinfo(
  216. self.configuration.qnap_poolname)
  217. total_capacity_gb = (int(selected_pool.find('capacity_bytes').text) /
  218. units.Gi)
  219. LOG.debug('total_capacity_gb: %s GB', total_capacity_gb)
  220. free_capacity_gb = (int(selected_pool.find('freesize_bytes').text) /
  221. units.Gi)
  222. LOG.debug('free_capacity_gb: %s GB', free_capacity_gb)
  223. alloc_capacity_gb = (int(selected_pool.find('allocated_bytes').text) /
  224. units.Gi)
  225. LOG.debug('allocated_capacity_gb: %s GB', alloc_capacity_gb)
  226. reserved_percentage = self.configuration.safe_get(
  227. 'reserved_share_percentage')
  228. # single pool now, need support multiple pools in the future
  229. single_pool = {
  230. "pool_name": self.configuration.qnap_poolname,
  231. "total_capacity_gb": total_capacity_gb,
  232. "free_capacity_gb": free_capacity_gb,
  233. "allocated_capacity_gb": alloc_capacity_gb,
  234. "reserved_percentage": reserved_percentage,
  235. "qos": False,
  236. "dedupe": [True, False],
  237. "compression": [True, False],
  238. "thin_provisioning": [True, False],
  239. "qnap_ssd_cache": [True, False]
  240. }
  241. data = {
  242. "share_backend_name": backend_name,
  243. "vendor_name": "QNAP",
  244. "driver_version": self.DRIVER_VERSION,
  245. "storage_protocol": "NFS",
  246. "snapshot_support": True,
  247. "create_share_from_snapshot_support": True,
  248. "driver_handles_share_servers": self.configuration.safe_get(
  249. 'driver_handles_share_servers'),
  250. 'pools': [single_pool],
  251. }
  252. super(QnapShareDriver, self)._update_share_stats(data)
  253. @utils.retry(exception=exception.ShareBackendException,
  254. interval=3,
  255. retries=5)
  256. @utils.synchronized('qnap-create_share')
  257. def create_share(self, context, share, share_server=None):
  258. """Create a new share."""
  259. LOG.debug('share: %s', share.__dict__)
  260. extra_specs = share_types.get_extra_specs_from_share(share)
  261. LOG.debug('extra_specs: %s', extra_specs)
  262. qnap_thin_provision = share_types.parse_boolean_extra_spec(
  263. 'thin_provisioning', extra_specs.get("thin_provisioning") or
  264. extra_specs.get('capabilities:thin_provisioning') or 'true')
  265. qnap_compression = share_types.parse_boolean_extra_spec(
  266. 'compression', extra_specs.get("compression") or
  267. extra_specs.get('capabilities:compression') or 'true')
  268. qnap_deduplication = share_types.parse_boolean_extra_spec(
  269. 'dedupe', extra_specs.get("dedupe") or
  270. extra_specs.get('capabilities:dedupe') or 'false')
  271. qnap_ssd_cache = share_types.parse_boolean_extra_spec(
  272. 'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
  273. extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
  274. LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
  275. 'qnap_compression: %(qnap_compression)s '
  276. 'qnap_deduplication: %(qnap_deduplication)s '
  277. 'qnap_ssd_cache: %(qnap_ssd_cache)s',
  278. {'qnap_thin_provision': qnap_thin_provision,
  279. 'qnap_compression': qnap_compression,
  280. 'qnap_deduplication': qnap_deduplication,
  281. 'qnap_ssd_cache': qnap_ssd_cache})
  282. share_proto = share['share_proto']
  283. # User could create two shares with the same name on horizon.
  284. # Therefore, we should not use displayname to create shares on NAS.
  285. create_share_name = self._gen_random_name("share")
  286. # If share name exists, need to change to another name.
  287. created_share = self.api_executor.get_share_info(
  288. self.configuration.qnap_poolname,
  289. vol_label=create_share_name)
  290. LOG.debug('created_share: %s', created_share)
  291. if created_share is not None:
  292. msg = (_("The share name %s is used by other share on NAS.") %
  293. create_share_name)
  294. LOG.error(msg)
  295. raise exception.ShareBackendException(msg=msg)
  296. if (qnap_deduplication and not qnap_thin_provision):
  297. msg = _("Dedupe cannot be enabled without thin_provisioning.")
  298. LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
  299. raise exception.InvalidExtraSpec(reason=msg)
  300. self.api_executor.create_share(
  301. share,
  302. self.configuration.qnap_poolname,
  303. create_share_name,
  304. share_proto,
  305. qnap_thin_provision=qnap_thin_provision,
  306. qnap_compression=qnap_compression,
  307. qnap_deduplication=qnap_deduplication,
  308. qnap_ssd_cache=qnap_ssd_cache)
  309. created_share = self._get_share_info(create_share_name)
  310. volID = created_share.find('vol_no').text
  311. # Use private_storage to record volume ID and Name created in the NAS.
  312. LOG.debug('volID: %(volID)s '
  313. 'volName: %(create_share_name)s',
  314. {'volID': volID,
  315. 'create_share_name': create_share_name})
  316. _metadata = {'volID': volID,
  317. 'volName': create_share_name,
  318. 'thin_provision': qnap_thin_provision,
  319. 'compression': qnap_compression,
  320. 'deduplication': qnap_deduplication,
  321. 'ssd_cache': qnap_ssd_cache}
  322. self.private_storage.update(share['id'], _metadata)
  323. return self._get_location_path(create_share_name,
  324. share['share_proto'],
  325. self.configuration.qnap_share_ip,
  326. volID)
  327. @utils.retry(exception=exception.ShareBackendException,
  328. interval=5, retries=5, backoff_rate=1)
  329. def _get_share_info(self, share_name):
  330. share = self.api_executor.get_share_info(
  331. self.configuration.qnap_poolname,
  332. vol_label=share_name)
  333. if share is None:
  334. msg = _("Fail to get share info of %s on NAS.") % share_name
  335. LOG.error(msg)
  336. raise exception.ShareBackendException(msg=msg)
  337. else:
  338. return share
  339. @utils.synchronized('qnap-delete_share')
  340. def delete_share(self, context, share, share_server=None):
  341. """Delete the specified share."""
  342. # Use private_storage to retrieve volume ID created in the NAS.
  343. volID = self.private_storage.get(share['id'], 'volID')
  344. if not volID:
  345. LOG.warning('volID for Share %s does not exist', share['id'])
  346. return
  347. LOG.debug('volID: %s', volID)
  348. del_share = self.api_executor.get_share_info(
  349. self.configuration.qnap_poolname,
  350. vol_no=volID)
  351. if del_share is None:
  352. LOG.warning('Share %s does not exist', share['id'])
  353. return
  354. vol_no = del_share.find('vol_no').text
  355. self.api_executor.delete_share(vol_no)
  356. self.private_storage.delete(share['id'])
  357. @utils.synchronized('qnap-extend_share')
  358. def extend_share(self, share, new_size, share_server=None):
  359. """Extend an existing share."""
  360. LOG.debug('Entering extend_share share_name=%(share_name)s '
  361. 'share_id=%(share_id)s '
  362. 'new_size=%(size)s',
  363. {'share_name': share['display_name'],
  364. 'share_id': share['id'],
  365. 'size': new_size})
  366. # Use private_storage to retrieve volume Name created in the NAS.
  367. volName = self.private_storage.get(share['id'], 'volName')
  368. if not volName:
  369. LOG.debug('Share %s does not exist', share['id'])
  370. raise exception.ShareResourceNotFound(share_id=share['id'])
  371. LOG.debug('volName: %s', volName)
  372. thin_provision = self.private_storage.get(
  373. share['id'], 'thin_provision')
  374. compression = self.private_storage.get(share['id'], 'compression')
  375. deduplication = self.private_storage.get(share['id'], 'deduplication')
  376. ssd_cache = self.private_storage.get(share['id'], 'ssd_cache')
  377. LOG.debug('thin_provision: %(thin_provision)s '
  378. 'compression: %(compression)s '
  379. 'deduplication: %(deduplication)s '
  380. 'ssd_cache: %(ssd_cache)s',
  381. {'thin_provision': thin_provision,
  382. 'compression': compression,
  383. 'deduplication': deduplication,
  384. 'ssd_cache': ssd_cache})
  385. share_dict = {
  386. 'sharename': volName,
  387. 'old_sharename': volName,
  388. 'new_size': new_size,
  389. 'thin_provision': thin_provision == 'True',
  390. 'compression': compression == 'True',
  391. 'deduplication': deduplication == 'True',
  392. 'ssd_cache': ssd_cache == 'True',
  393. 'share_proto': share['share_proto']
  394. }
  395. self.api_executor.edit_share(share_dict)
  396. @utils.retry(exception=exception.ShareBackendException,
  397. interval=3,
  398. retries=5)
  399. @utils.synchronized('qnap-create_snapshot')
  400. def create_snapshot(self, context, snapshot, share_server=None):
  401. """Create a snapshot."""
  402. LOG.debug('snapshot[share][share_id]: %s',
  403. snapshot['share']['share_id'])
  404. LOG.debug('snapshot id: %s', snapshot['id'])
  405. # Use private_storage to retrieve volume ID created in the NAS.
  406. volID = self.private_storage.get(snapshot['share']['id'], 'volID')
  407. if not volID:
  408. LOG.warning(
  409. 'volID for Share %s does not exist',
  410. snapshot['share']['id'])
  411. raise exception.ShareResourceNotFound(
  412. share_id=snapshot['share']['id'])
  413. LOG.debug('volID: %s', volID)
  414. # User could create two snapshot with the same name on horizon.
  415. # Therefore, we should not use displayname to create snapshot on NAS.
  416. # if snapshot exist, need to change another
  417. create_snapshot_name = self._gen_random_name("snapshot")
  418. LOG.debug('create_snapshot_name: %s', create_snapshot_name)
  419. check_snapshot = self.api_executor.get_snapshot_info(
  420. volID=volID, snapshot_name=create_snapshot_name)
  421. if check_snapshot is not None:
  422. msg = _("Failed to create an unused snapshot name.")
  423. raise exception.ShareBackendException(msg=msg)
  424. LOG.debug('create_snapshot_name: %s', create_snapshot_name)
  425. self.api_executor.create_snapshot_api(volID, create_snapshot_name)
  426. snapshot_id = ""
  427. created_snapshot = self.api_executor.get_snapshot_info(
  428. volID=volID, snapshot_name=create_snapshot_name)
  429. if created_snapshot is not None:
  430. snapshot_id = created_snapshot.find('snapshot_id').text
  431. else:
  432. msg = _("Failed to get snapshot information.")
  433. raise exception.ShareBackendException(msg=msg)
  434. LOG.debug('created_snapshot: %s', created_snapshot)
  435. LOG.debug('snapshot_id: %s', snapshot_id)
  436. # Use private_storage to record data instead of metadata.
  437. _metadata = {'snapshot_id': snapshot_id}
  438. self.private_storage.update(snapshot['id'], _metadata)
  439. # Test to get value from private_storage.
  440. snapshot_id = self.private_storage.get(snapshot['id'], 'snapshot_id')
  441. LOG.debug('snapshot_id: %s', snapshot_id)
  442. return {'provider_location': snapshot_id}
  443. @utils.synchronized('qnap-delete_snapshot')
  444. def delete_snapshot(self, context, snapshot, share_server=None):
  445. """Delete a snapshot."""
  446. LOG.debug('Entering delete_snapshot. The deleted snapshot=%(snap)s',
  447. {'snap': snapshot['id']})
  448. snapshot_id = (snapshot.get('provider_location') or
  449. self.private_storage.get(snapshot['id'], 'snapshot_id'))
  450. if not snapshot_id:
  451. LOG.warning('Snapshot %s does not exist', snapshot['id'])
  452. return
  453. LOG.debug('snapshot_id: %s', snapshot_id)
  454. self.api_executor.delete_snapshot_api(snapshot_id)
  455. self.private_storage.delete(snapshot['id'])
  456. @utils.retry(exception=exception.ShareBackendException,
  457. interval=3,
  458. retries=5)
  459. @utils.synchronized('qnap-create_share_from_snapshot')
  460. def create_share_from_snapshot(self, context, share, snapshot,
  461. share_server=None):
  462. """Create a share from a snapshot."""
  463. LOG.debug('Entering create_share_from_snapshot. The source '
  464. 'snapshot=%(snap)s. The created share=%(share)s',
  465. {'snap': snapshot['id'], 'share': share['id']})
  466. snapshot_id = (snapshot.get('provider_location') or
  467. self.private_storage.get(snapshot['id'], 'snapshot_id'))
  468. if not snapshot_id:
  469. LOG.warning('Snapshot %s does not exist', snapshot['id'])
  470. raise exception.SnapshotResourceNotFound(name=snapshot['id'])
  471. LOG.debug('snapshot_id: %s', snapshot_id)
  472. create_share_name = self._gen_random_name("share")
  473. # if sharename exist, need to change another
  474. created_share = self.api_executor.get_share_info(
  475. self.configuration.qnap_poolname,
  476. vol_label=create_share_name)
  477. if created_share is not None:
  478. msg = _("Failed to create an unused share name.")
  479. raise exception.ShareBackendException(msg=msg)
  480. self.api_executor.clone_snapshot(snapshot_id, create_share_name)
  481. create_volID = ""
  482. created_share = self.api_executor.get_share_info(
  483. self.configuration.qnap_poolname,
  484. vol_label=create_share_name)
  485. if created_share is not None:
  486. create_volID = created_share.find('vol_no').text
  487. LOG.debug('create_volID: %s', create_volID)
  488. else:
  489. msg = _("Failed to clone a snapshot in time.")
  490. raise exception.ShareBackendException(msg=msg)
  491. snap_share = self.share_api.get(
  492. context, snapshot['share_instance']['share_id'])
  493. LOG.debug('snap_share[size]: %s', snap_share['size'])
  494. thin_provision = self.private_storage.get(
  495. snapshot['share_instance_id'], 'thin_provision')
  496. compression = self.private_storage.get(
  497. snapshot['share_instance_id'], 'compression')
  498. deduplication = self.private_storage.get(
  499. snapshot['share_instance_id'], 'deduplication')
  500. ssd_cache = self.private_storage.get(
  501. snapshot['share_instance_id'], 'ssd_cache')
  502. LOG.debug('thin_provision: %(thin_provision)s '
  503. 'compression: %(compression)s '
  504. 'deduplication: %(deduplication)s '
  505. 'ssd_cache: %(ssd_cache)s',
  506. {'thin_provision': thin_provision,
  507. 'compression': compression,
  508. 'deduplication': deduplication,
  509. 'ssd_cache': ssd_cache})
  510. if (share['size'] > snap_share['size']):
  511. share_dict = {
  512. 'sharename': create_share_name,
  513. 'old_sharename': create_share_name,
  514. 'new_size': share['size'],
  515. 'thin_provision': thin_provision == 'True',
  516. 'compression': compression == 'True',
  517. 'deduplication': deduplication == 'True',
  518. 'ssd_cache': ssd_cache == 'True',
  519. 'share_proto': share['share_proto']
  520. }
  521. self.api_executor.edit_share(share_dict)
  522. # Use private_storage to record volume ID and Name created in the NAS.
  523. _metadata = {
  524. 'volID': create_volID,
  525. 'volName': create_share_name,
  526. 'thin_provision': thin_provision,
  527. 'compression': compression,
  528. 'deduplication': deduplication,
  529. 'ssd_cache': ssd_cache
  530. }
  531. self.private_storage.update(share['id'], _metadata)
  532. # Test to get value from private_storage.
  533. volName = self.private_storage.get(share['id'], 'volName')
  534. LOG.debug('volName: %s', volName)
  535. return self._get_location_path(create_share_name,
  536. share['share_proto'],
  537. self.configuration.qnap_share_ip,
  538. create_volID)
  539. def _get_vol_host(self, host_list, vol_name_timestamp):
  540. vol_host_list = []
  541. if host_list is None:
  542. return vol_host_list
  543. for host in host_list:
  544. # Check host alias name with prefix "manila-{vol_name_timestamp}"
  545. # to find the host of this manila share.
  546. LOG.debug('_get_vol_host name:%s', host.find('name').text)
  547. # Because driver supports only IPv4 now, check "netaddrs"
  548. # have "ipv4" tag to get address.
  549. if re.match("^manila-{}".format(vol_name_timestamp),
  550. host.find('name').text):
  551. host_dict = {
  552. 'index': host.find('index').text,
  553. 'hostid': host.find('hostid').text,
  554. 'name': host.find('name').text,
  555. 'ipv4': [],
  556. }
  557. for ipv4 in host.findall('netaddrs/ipv4'):
  558. host_dict['ipv4'].append(ipv4.text)
  559. vol_host_list.append(host_dict)
  560. LOG.debug('_get_vol_host vol_host_list:%s', vol_host_list)
  561. return vol_host_list
  562. @utils.synchronized('qnap-update_access')
  563. def update_access(self, context, share, access_rules, add_rules,
  564. delete_rules, share_server=None):
  565. if not (add_rules or delete_rules):
  566. volName = self.private_storage.get(share['id'], 'volName')
  567. LOG.debug('volName: %s', volName)
  568. if volName is None:
  569. LOG.debug('Share %s does not exist', share['id'])
  570. raise exception.ShareResourceNotFound(share_id=share['id'])
  571. # Clear all current ACLs
  572. self.api_executor.set_nfs_access(volName, 2, "all")
  573. vol_name_timestamp = self._get_timestamp_from_vol_name(volName)
  574. host_list = self.api_executor.get_host_list()
  575. LOG.debug('host_list:%s', host_list)
  576. vol_host_list = self._get_vol_host(host_list, vol_name_timestamp)
  577. # If host already exist, delete the host
  578. if len(vol_host_list) > 0:
  579. for vol_host in vol_host_list:
  580. self.api_executor.delete_host(vol_host['name'])
  581. # Add each one through all rules.
  582. for access in access_rules:
  583. self._allow_access(context, share, access, share_server)
  584. else:
  585. # Adding/Deleting specific rules
  586. for access in delete_rules:
  587. self._deny_access(context, share, access, share_server)
  588. for access in add_rules:
  589. self._allow_access(context, share, access, share_server)
  590. def _allow_access(self, context, share, access, share_server=None):
  591. """Allow access to the share."""
  592. share_proto = share['share_proto']
  593. access_type = access['access_type']
  594. access_level = access['access_level']
  595. access_to = access['access_to']
  596. LOG.debug('share_proto: %(share_proto)s '
  597. 'access_type: %(access_type)s'
  598. 'access_level: %(access_level)s'
  599. 'access_to: %(access_to)s',
  600. {'share_proto': share_proto,
  601. 'access_type': access_type,
  602. 'access_level': access_level,
  603. 'access_to': access_to})
  604. self._check_share_access(share_proto, access_type)
  605. vol_name = self.private_storage.get(share['id'], 'volName')
  606. vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
  607. host_name = self._gen_host_name(vol_name_timestamp, access_level)
  608. host_list = self.api_executor.get_host_list()
  609. LOG.debug('vol_name: %(vol_name)s '
  610. 'access_level: %(access_level)s '
  611. 'host_name: %(host_name)s '
  612. 'host_list: %(host_list)s ',
  613. {'vol_name': vol_name,
  614. 'access_level': access_level,
  615. 'host_name': host_name,
  616. 'host_list': host_list})
  617. filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
  618. if len(filter_host_list) == 0:
  619. # if host does not exist, create a host for the share
  620. self.api_executor.add_host(host_name, access_to)
  621. elif (len(filter_host_list) == 1 and
  622. filter_host_list[0]['name'] == host_name):
  623. # if the host exist, and this host is for the same access right,
  624. # add ip to the host.
  625. ipv4_list = filter_host_list[0]['ipv4']
  626. if access_to not in ipv4_list:
  627. ipv4_list.append(access_to)
  628. LOG.debug('vol_host["ipv4"]: %s', filter_host_list[0]['ipv4'])
  629. LOG.debug('ipv4_list: %s', ipv4_list)
  630. self.api_executor.edit_host(host_name, ipv4_list)
  631. else:
  632. # Until now, share of QNAP NAS can only apply one access level for
  633. # all ips. "rw" for some ips and "ro" for else is not allowed.
  634. support_level = (constants.ACCESS_LEVEL_RW if
  635. access_level == constants.ACCESS_LEVEL_RO
  636. else constants.ACCESS_LEVEL_RO)
  637. reason = _('Share only supports one access '
  638. 'level: %s') % support_level
  639. LOG.error(reason)
  640. raise exception.InvalidShareAccess(reason=reason)
  641. access = 1 if access_level == constants.ACCESS_LEVEL_RO else 0
  642. self.api_executor.set_nfs_access(vol_name, access, host_name)
  643. def _deny_access(self, context, share, access, share_server=None):
  644. """Deny access to the share."""
  645. share_proto = share['share_proto']
  646. access_type = access['access_type']
  647. access_level = access['access_level']
  648. access_to = access['access_to']
  649. LOG.debug('share_proto: %(share_proto)s '
  650. 'access_type: %(access_type)s'
  651. 'access_level: %(access_level)s'
  652. 'access_to: %(access_to)s',
  653. {'share_proto': share_proto,
  654. 'access_type': access_type,
  655. 'access_level': access_level,
  656. 'access_to': access_to})
  657. try:
  658. self._check_share_access(share_proto, access_type)
  659. except exception.InvalidShareAccess:
  660. LOG.warning('The denied rule is invalid and does not exist.')
  661. return
  662. vol_name = self.private_storage.get(share['id'], 'volName')
  663. vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
  664. host_name = self._gen_host_name(vol_name_timestamp, access_level)
  665. host_list = self.api_executor.get_host_list()
  666. LOG.debug('vol_name: %(vol_name)s '
  667. 'access_level: %(access_level)s '
  668. 'host_name: %(host_name)s '
  669. 'host_list: %(host_list)s ',
  670. {'vol_name': vol_name,
  671. 'access_level': access_level,
  672. 'host_name': host_name,
  673. 'host_list': host_list})
  674. filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
  675. # if share already have host, remove ip from host
  676. for vol_host in filter_host_list:
  677. if vol_host['name'] == host_name:
  678. ipv4_list = vol_host['ipv4']
  679. if access_to in ipv4_list:
  680. ipv4_list.remove(access_to)
  681. LOG.debug('vol_host["ipv4"]: %s', vol_host['ipv4'])
  682. LOG.debug('ipv4_list: %s', ipv4_list)
  683. if len(ipv4_list) == 0: # if list empty, remove the host
  684. self.api_executor.set_nfs_access(
  685. vol_name, 2, host_name)
  686. self.api_executor.delete_host(host_name)
  687. else:
  688. self.api_executor.edit_host(host_name, ipv4_list)
  689. break
  690. def _check_share_access(self, share_proto, access_type):
  691. if share_proto == 'NFS' and access_type != 'ip':
  692. reason = _('Only "ip" access type is allowed for '
  693. 'NFS shares.')
  694. LOG.warning(reason)
  695. raise exception.InvalidShareAccess(reason=reason)
  696. elif share_proto != 'NFS':
  697. reason = _('Invalid NAS protocol: %s') % share_proto
  698. raise exception.InvalidShareAccess(reason=reason)
  699. def manage_existing(self, share, driver_options):
  700. """Manages a share that exists on backend."""
  701. if share['share_proto'].lower() == 'nfs':
  702. # 10.0.0.1:/share/example
  703. LOG.info("Share %(shr_path)s will be managed with ID "
  704. "%(shr_id)s.",
  705. {'shr_path': share['export_locations'][0]['path'],
  706. 'shr_id': share['id']})
  707. old_path_info = share['export_locations'][0]['path'].split(
  708. ':/share/')
  709. if len(old_path_info) == 2:
  710. ip = old_path_info[0]
  711. share_name = old_path_info[1]
  712. else:
  713. msg = _("Incorrect path. It should have the following format: "
  714. "IP:/share/share_name.")
  715. raise exception.ShareBackendException(msg=msg)
  716. else:
  717. msg = _('Invalid NAS protocol: %s') % share['share_proto']
  718. raise exception.InvalidInput(reason=msg)
  719. if ip != self.configuration.qnap_share_ip:
  720. msg = _("The NAS IP %(ip)s is not configured.") % {'ip': ip}
  721. raise exception.ShareBackendException(msg=msg)
  722. existing_share = self.api_executor.get_share_info(
  723. self.configuration.qnap_poolname,
  724. vol_label=share_name)
  725. if existing_share is None:
  726. msg = _("The share %s trying to be managed was not found on "
  727. "backend.") % share['id']
  728. raise exception.ManageInvalidShare(reason=msg)
  729. extra_specs = share_types.get_extra_specs_from_share(share)
  730. qnap_thin_provision = share_types.parse_boolean_extra_spec(
  731. 'thin_provisioning', extra_specs.get("thin_provisioning") or
  732. extra_specs.get('capabilities:thin_provisioning') or 'true')
  733. qnap_compression = share_types.parse_boolean_extra_spec(
  734. 'compression', extra_specs.get("compression") or
  735. extra_specs.get('capabilities:compression') or 'true')
  736. qnap_deduplication = share_types.parse_boolean_extra_spec(
  737. 'dedupe', extra_specs.get("dedupe") or
  738. extra_specs.get('capabilities:dedupe') or 'false')
  739. qnap_ssd_cache = share_types.parse_boolean_extra_spec(
  740. 'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
  741. extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
  742. LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
  743. 'qnap_compression: %(qnap_compression)s '
  744. 'qnap_deduplication: %(qnap_deduplication)s '
  745. 'qnap_ssd_cache: %(qnap_ssd_cache)s',
  746. {'qnap_thin_provision': qnap_thin_provision,
  747. 'qnap_compression': qnap_compression,
  748. 'qnap_deduplication': qnap_deduplication,
  749. 'qnap_ssd_cache': qnap_ssd_cache})
  750. if (qnap_deduplication and not qnap_thin_provision):
  751. msg = _("Dedupe cannot be enabled without thin_provisioning.")
  752. LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
  753. raise exception.InvalidExtraSpec(reason=msg)
  754. vol_no = existing_share.find('vol_no').text
  755. vol = self.api_executor.get_specific_volinfo(vol_no)
  756. vol_size_gb = math.ceil(float(vol.find('size').text) / units.Gi)
  757. share_dict = {
  758. 'sharename': share_name,
  759. 'old_sharename': share_name,
  760. 'thin_provision': qnap_thin_provision,
  761. 'compression': qnap_compression,
  762. 'deduplication': qnap_deduplication,
  763. 'ssd_cache': qnap_ssd_cache,
  764. 'share_proto': share['share_proto']
  765. }
  766. self.api_executor.edit_share(share_dict)
  767. _metadata = {}
  768. _metadata['volID'] = vol_no
  769. _metadata['volName'] = share_name
  770. _metadata['thin_provision'] = qnap_thin_provision
  771. _metadata['compression'] = qnap_compression
  772. _metadata['deduplication'] = qnap_deduplication
  773. _metadata['ssd_cache'] = qnap_ssd_cache
  774. self.private_storage.update(share['id'], _metadata)
  775. LOG.info("Share %(shr_path)s was successfully managed with ID "
  776. "%(shr_id)s.",
  777. {'shr_path': share['export_locations'][0]['path'],
  778. 'shr_id': share['id']})
  779. export_locations = self._get_location_path(
  780. share_name,
  781. share['share_proto'],
  782. self.configuration.qnap_share_ip,
  783. vol_no)
  784. return {'size': vol_size_gb, 'export_locations': export_locations}
  785. def unmanage(self, share):
  786. """Remove the specified share from Manila management."""
  787. self.private_storage.delete(share['id'])
  788. def manage_existing_snapshot(self, snapshot, driver_options):
  789. """Manage existing share snapshot with manila."""
  790. volID = self.private_storage.get(snapshot['share']['id'], 'volID')
  791. LOG.debug('volID: %s', volID)
  792. existing_share = self.api_executor.get_share_info(
  793. self.configuration.qnap_poolname,
  794. vol_no=volID)
  795. if existing_share is None:
  796. msg = _("The share id %s was not found on backend.") % volID
  797. LOG.error(msg)
  798. raise exception.ShareNotFound(reason=msg)
  799. snapshot_id = snapshot.get('provider_location')
  800. snapshot_id_info = snapshot_id.split('@')
  801. if len(snapshot_id_info) == 2:
  802. share_name = snapshot_id_info[0]
  803. snapshot_name = snapshot_id_info[1]
  804. else:
  805. msg = _("Incorrect provider_location format. It should have the "
  806. "following format: share_name@snapshot_name.")
  807. LOG.error(msg)
  808. raise exception.InvalidParameterValue(reason=msg)
  809. if share_name != existing_share.find('vol_label').text:
  810. msg = (_("The assigned share %(share_name)s was not matched "
  811. "%(vol_label)s on backend.") %
  812. {'share_name': share_name,
  813. 'vol_label': existing_share.find('vol_label').text})
  814. LOG.error(msg)
  815. raise exception.ShareNotFound(reason=msg)
  816. check_snapshot = self.api_executor.get_snapshot_info(
  817. volID=volID, snapshot_name=snapshot_name)
  818. if check_snapshot is None:
  819. msg = (_("The snapshot %(snapshot_name)s was not "
  820. "found on backend.") %
  821. {'snapshot_name': snapshot_name})
  822. LOG.error(msg)
  823. raise exception.InvalidParameterValue(err=msg)
  824. _metadata = {
  825. 'snapshot_id': snapshot_id,
  826. }
  827. self.private_storage.update(snapshot['id'], _metadata)
  828. def unmanage_snapshot(self, snapshot):
  829. """Remove the specified snapshot from Manila management."""
  830. self.private_storage.delete(snapshot['id'])