manila/manila/share/drivers/windows/windows_smb_helper.py

268 lines
11 KiB
Python

# Copyright (c) 2015 Cloudbase Solutions SRL
# 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.
import json
import os
from oslo_log import log
from manila.common import constants
from manila import exception
from manila.share.drivers import helpers
from manila.share.drivers.windows import windows_utils
LOG = log.getLogger(__name__)
class WindowsSMBHelper(helpers.CIFSHelperBase):
_SHARE_ACCESS_RIGHT_MAP = {
constants.ACCESS_LEVEL_RW: "Change",
constants.ACCESS_LEVEL_RO: "Read"}
_NULL_SID = "S-1-0-0"
_WIN_ACL_ALLOW = 0
_WIN_ACL_DENY = 1
_WIN_ACCESS_RIGHT_FULL = 0
_WIN_ACCESS_RIGHT_CHANGE = 1
_WIN_ACCESS_RIGHT_READ = 2
_WIN_ACCESS_RIGHT_CUSTOM = 3
_ACCESS_LEVEL_CUSTOM = 'custom'
_WIN_ACL_MAP = {
_WIN_ACCESS_RIGHT_CHANGE: constants.ACCESS_LEVEL_RW,
_WIN_ACCESS_RIGHT_FULL: constants.ACCESS_LEVEL_RW,
_WIN_ACCESS_RIGHT_READ: constants.ACCESS_LEVEL_RO,
_WIN_ACCESS_RIGHT_CUSTOM: _ACCESS_LEVEL_CUSTOM,
}
_SUPPORTED_ACCESS_LEVELS = (constants.ACCESS_LEVEL_RO,
constants.ACCESS_LEVEL_RW)
_SUPPORTED_ACCESS_TYPES = ('user', )
def __init__(self, remote_execute, configuration):
self._remote_exec = remote_execute
self.configuration = configuration
self._windows_utils = windows_utils.WindowsUtils(
remote_execute=remote_execute)
def init_helper(self, server):
self._remote_exec(server, "Get-SmbShare")
def create_exports(self, server, share_name, recreate=False):
export_location = '\\\\%s\\%s' % (server['public_address'],
share_name)
if not self._share_exists(server, share_name):
share_path = self._windows_utils.normalize_path(
os.path.join(self.configuration.share_mount_path,
share_name))
# If no access rules are requested, 'Everyone' will have read
# access, by default. We set read access for the 'NULL SID' in
# order to avoid this.
cmd = ['New-SmbShare', '-Name', share_name, '-Path', share_path,
'-ReadAccess', "*%s" % self._NULL_SID]
self._remote_exec(server, cmd)
else:
LOG.info("Skipping creating export %s as it already exists.",
share_name)
return self.get_exports_for_share(server, export_location)
def remove_exports(self, server, share_name):
if self._share_exists(server, share_name):
cmd = ['Remove-SmbShare', '-Name', share_name, "-Force"]
self._remote_exec(server, cmd)
else:
LOG.debug("Skipping removing export %s as it does not exist.",
share_name)
def _get_volume_path_by_share_name(self, server, share_name):
share_path = self._get_share_path_by_name(server, share_name)
volume_path = self._windows_utils.get_volume_path_by_mount_path(
server, share_path)
return volume_path
def _get_acls(self, server, share_name):
cmd = ('Get-SmbShareAccess -Name %(share_name)s | '
'Select-Object @("Name", "AccountName", '
'"AccessControlType", "AccessRight") | '
'ConvertTo-JSON -Compress' % {'share_name': share_name})
(out, err) = self._remote_exec(server, cmd)
if not out.strip():
return []
raw_acls = json.loads(out)
if isinstance(raw_acls, dict):
return [raw_acls]
return raw_acls
def get_access_rules(self, server, share_name):
raw_acls = self._get_acls(server, share_name)
acls = []
for raw_acl in raw_acls:
access_to = raw_acl['AccountName']
access_right = raw_acl['AccessRight']
access_level = self._WIN_ACL_MAP[access_right]
access_allow = raw_acl["AccessControlType"] == self._WIN_ACL_ALLOW
if not access_allow:
if access_to.lower() == 'everyone' and len(raw_acls) == 1:
LOG.debug("No access rules are set yet for share %s",
share_name)
else:
LOG.warning(
"Found explicit deny ACE rule that was not "
"created by Manila and will be ignored: %s",
raw_acl)
continue
if access_level == self._ACCESS_LEVEL_CUSTOM:
LOG.warning(
"Found 'custom' ACE rule that will be ignored: %s",
raw_acl)
continue
elif access_right == self._WIN_ACCESS_RIGHT_FULL:
LOG.warning(
"Account '%(access_to)s' was given full access "
"right on share %(share_name)s. Manila only "
"grants 'change' access.",
{'access_to': access_to,
'share_name': share_name})
acl = {
'access_to': access_to,
'access_level': access_level,
'access_type': 'user',
}
acls.append(acl)
return acls
def _grant_share_access(self, server, share_name, access_level, access_to):
access_right = self._SHARE_ACCESS_RIGHT_MAP[access_level]
cmd = ["Grant-SmbShareAccess", "-Name", share_name,
"-AccessRight", access_right,
"-AccountName", "'%s'" % access_to, "-Force"]
self._remote_exec(server, cmd)
self._refresh_acl(server, share_name)
LOG.info("Granted %(access_level)s access to '%(access_to)s' "
"on share %(share_name)s",
{'access_level': access_level,
'access_to': access_to,
'share_name': share_name})
def _refresh_acl(self, server, share_name):
cmd = ['Set-SmbPathAcl', '-ShareName', share_name]
self._remote_exec(server, cmd)
def _revoke_share_access(self, server, share_name, access_to):
cmd = ['Revoke-SmbShareAccess', '-Name', share_name,
'-AccountName', '"%s"' % access_to, '-Force']
self._remote_exec(server, cmd)
self._refresh_acl(server, share_name)
LOG.info("Revoked access to '%(access_to)s' "
"on share %(share_name)s",
{'access_to': access_to,
'share_name': share_name})
def update_access(self, server, share_name, access_rules, add_rules,
delete_rules):
self.validate_access_rules(
access_rules + add_rules,
self._SUPPORTED_ACCESS_TYPES,
self._SUPPORTED_ACCESS_LEVELS)
if not (add_rules or delete_rules):
existing_rules = self.get_access_rules(server, share_name)
add_rules, delete_rules = self._get_rule_updates(
existing_rules=existing_rules,
requested_rules=access_rules)
LOG.debug(("Missing rules: %(add_rules)s, "
"superfluous rules: %(delete_rules)s"),
{'add_rules': add_rules,
'delete_rules': delete_rules})
# Some rules may have changed, so we'll
# treat the deleted rules first.
for deleted_rule in delete_rules:
try:
self.validate_access_rules(
[deleted_rule],
self._SUPPORTED_ACCESS_TYPES,
self._SUPPORTED_ACCESS_LEVELS)
except (exception.InvalidShareAccess,
exception.InvalidShareAccessLevel):
# This check will allow invalid rules to be deleted.
LOG.warning(
"Unsupported access level %(level)s or access type "
"%(type)s, skipping removal of access rule to "
"%(to)s.", {'level': deleted_rule['access_level'],
'type': deleted_rule['access_type'],
'to': deleted_rule['access_to']})
continue
self._revoke_share_access(server, share_name,
deleted_rule['access_to'])
for added_rule in add_rules:
self._grant_share_access(server, share_name,
added_rule['access_level'],
added_rule['access_to'])
def _subtract_access_rules(self, access_rules, subtracted_rules):
# Account names are case insensitive on Windows.
filter_rules = lambda rules: [
{'access_to': access_rule['access_to'].lower(),
'access_level': access_rule['access_level'],
'access_type': access_rule['access_type']}
for access_rule in rules]
return [rule for rule in filter_rules(access_rules)
if rule not in filter_rules(subtracted_rules)]
def _get_rule_updates(self, existing_rules, requested_rules):
added_rules = self._subtract_access_rules(requested_rules,
existing_rules)
deleted_rules = self._subtract_access_rules(existing_rules,
requested_rules)
return added_rules, deleted_rules
def _get_share_name(self, export_location):
return self._windows_utils.normalize_path(
export_location).split('\\')[-1]
def _get_export_location_template(self, old_export_location):
share_name = self._get_share_name(old_export_location)
return '\\\\%s' + ('\\%s' % share_name)
def _get_share_path_by_name(self, server, share_name,
ignore_missing=False):
cmd = ('Get-SmbShare -Name %s | '
'Select-Object -ExpandProperty Path' % share_name)
check_exit_code = not ignore_missing
(share_path, err) = self._remote_exec(server, cmd,
check_exit_code=check_exit_code)
return share_path.strip() if share_path else None
def get_share_path_by_export_location(self, server, export_location):
share_name = self._get_share_name(export_location)
return self._get_share_path_by_name(server, share_name)
def _share_exists(self, server, share_name):
share_path = self._get_share_path_by_name(server, share_name,
ignore_missing=True)
return bool(share_path)