Skip to content

Commit

Permalink
NAS-130390 / 24.10 / Move discovery auth from per-portal to system-wi…
Browse files Browse the repository at this point in the history
…de (#14198)

- Perform database migration
- Generate alerts from migration if appropriate.  Uses keyvalue
- Add new iscsi.discoveryauth.* APIs
- Stub out auth aspect of iscsi.portal.* APIs, for later removal
- Add code to clear ISCSIDiscoveryAuth alerts
  • Loading branch information
bmeagherix authored Aug 13, 2024
1 parent 5eb8fae commit 5447c45
Show file tree
Hide file tree
Showing 6 changed files with 477 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Add iSCSI discoverauth
Flatten the per-portal discovery auth to a system-wide discovery auth.
Revision ID: 504a7bd32680
Revises: 4b0b7ba46e63
Create Date: 2024-08-12 15:53:48.342351+00:00
"""
import json

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '504a7bd32680'
down_revision = '4b0b7ba46e63'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('services_iscsidiscoveryauth',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('iscsi_discoveryauth_authmethod', sa.String(length=120), nullable=False),
sa.Column('iscsi_discoveryauth_authgroup', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_services_iscsidiscoveryauth')),
sa.UniqueConstraint('iscsi_discoveryauth_authgroup', name=op.f('uq_services_iscsidiscoveryauth_iscsi_discoveryauth_authgroup')),
sqlite_autoincrement=True
)

conn = op.get_bind()
data = conn.execute("SELECT iscsi_target_portal_discoveryauthgroup, iscsi_target_portal_discoveryauthmethod, id FROM services_iscsitargetportal").fetchall()

# Migrate the data into the new table.
# - Mutual CHAP first.
mutual_chap_auth_groups = []
for authgroup, authmethod, _portal_id in data:
if authmethod == 'CHAP Mutual' and authgroup not in mutual_chap_auth_groups:
# Let's not carry around 'CHAP Mutual' anymore.
conn.execute('INSERT INTO services_iscsidiscoveryauth (iscsi_discoveryauth_authmethod, iscsi_discoveryauth_authgroup) VALUES ("CHAP_MUTUAL",?)', authgroup)
mutual_chap_auth_groups.append(authgroup)
# - Simple CHAP next.
simple_chap_auth_groups = []
for authgroup, authmethod, _portal_id in data:
if authmethod == 'CHAP' and authgroup not in mutual_chap_auth_groups + simple_chap_auth_groups:
conn.execute('INSERT INTO services_iscsidiscoveryauth (iscsi_discoveryauth_authmethod, iscsi_discoveryauth_authgroup) VALUES ("CHAP",?)', authgroup)
simple_chap_auth_groups.append(authgroup)

# Things to test
# 1. Do we have a mix of None and non-None?
# 2. If not, do we have more than one CHAP/Mutual CHAP
# 3. Do we have more than one Mutual CHAP peeruser/secret?
none_list = list(filter(lambda t: t[1] == 'None', data))
none_count = len(none_list)
non_none_count = len(mutual_chap_auth_groups) + len(simple_chap_auth_groups)

if none_count and non_none_count:
portal_id_strs = list(str(item[2]) for item in none_list)
none_ips = conn.execute("SELECT iscsi_target_portalip_ip FROM services_iscsitargetportalip WHERE iscsi_target_portalip_portal_id IN (?)", ','.join(portal_id_strs)).fetchall()
conn.execute("INSERT INTO system_keyvalue (\"key\", value) VALUES (?, ?)",
("ISCSIDiscoveryAuthMixed", json.dumps({'ips': [ip[0] for ip in none_ips]})))
elif non_none_count > 1:
conn.execute("INSERT INTO system_keyvalue (\"key\", value) VALUES (?, ?)",
("ISCSIDiscoveryAuthMultipleCHAP", json.dumps({})))

if mutual_chap_auth_groups:
if len(mutual_chap_auth_groups) == 1:
data = conn.execute(f"SELECT DISTINCT iscsi_target_auth_peeruser FROM services_iscsitargetauthcredential WHERE iscsi_target_auth_tag = {mutual_chap_auth_groups[0]} AND iscsi_target_auth_peeruser != ''").fetchall()
else:
tags = ','.join(str(x) for x in mutual_chap_auth_groups)
data = conn.execute(f"SELECT DISTINCT iscsi_target_auth_peeruser FROM services_iscsitargetauthcredential WHERE iscsi_target_auth_tag in ({tags}) AND iscsi_target_auth_peeruser != ''").fetchall()
if len(list(data)) > 1:
active_peeruser = data[0][0]
conn.execute("INSERT INTO system_keyvalue (\"key\", value) VALUES (?, ?)",
("ISCSIDiscoveryAuthMultipleMutualCHAP", json.dumps({'peeruser': active_peeruser})))

# Remove the obsolete columns
with op.batch_alter_table('services_iscsitargetportal', schema=None) as batch_op:
batch_op.drop_column('iscsi_target_portal_discoveryauthgroup')
batch_op.drop_column('iscsi_target_portal_discoveryauthmethod')


def downgrade():
pass
24 changes: 24 additions & 0 deletions src/middlewared/middlewared/alert/source/discovery_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from middlewared.alert.base import AlertCategory, AlertClass, AlertLevel, SimpleOneShotAlertClass

UPGRADE_ALERTS = ['ISCSIDiscoveryAuthMixed', 'ISCSIDiscoveryAuthMultipleCHAP', 'ISCSIDiscoveryAuthMultipleMutualCHAP']


class ISCSIDiscoveryAuthMixedAlertClass(AlertClass, SimpleOneShotAlertClass):
category = AlertCategory.SHARING
level = AlertLevel.WARNING
title = "iSCSI Discovery Authorization Global"
text = "Prior to upgrade had specified iSCSI discovery auth on only some portals, now applies globally. May need to update client configuration when using %(ips)s"


class ISCSIDiscoveryAuthMultipleCHAPAlertClass(AlertClass, SimpleOneShotAlertClass):
category = AlertCategory.SHARING
level = AlertLevel.WARNING
title = "iSCSI Discovery Authorization merged"
text = "Prior to upgrade different portals had different iSCSI discovery auth, now applies globally."


class ISCSIDiscoveryAuthMultipleMutualCHAPAlertClass(AlertClass, SimpleOneShotAlertClass):
category = AlertCategory.SHARING
level = AlertLevel.WARNING
title = "iSCSI Discovery Authorization Multiple Mutual CHAP"
text = "Multiple mutual CHAP peers defined for discovery auth, but only first one (\"%(peeruser)s\") applies. May need to update client configuration."
31 changes: 23 additions & 8 deletions src/middlewared/middlewared/plugins/iscsi_/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ async def do_create(self, data):

verrors.check()

orig_peerusers = await self.middleware.call('iscsi.discoveryauth.mutual_chap_peerusers')

data['id'] = await self.middleware.call(
'datastore.insert', self._config.datastore, data,
{'prefix': self._config.datastore_prefix}
)

await self.middleware.call('iscsi.discoveryauth.recalc_mutual_chap_alert', orig_peerusers)
await self._service_change('iscsitarget', 'reload')

return await self.get_instance(data['id'])
Expand Down Expand Up @@ -95,17 +98,20 @@ async def do_update(self, audit_callback, id_, data):
verrors = ValidationErrors()
await self.validate(new, 'iscsi_auth_update', verrors)
if new['tag'] != old['tag'] and not await self.query([['tag', '=', old['tag']], ['id', '!=', id_]]):
usages = await self.is_in_use_by_portals_targets(id_)
usages = await self.is_in_use(id_)
if usages['in_use']:
verrors.add('iscsi_auth_update.tag', usages['usages'])

verrors.check()

orig_peerusers = await self.middleware.call('iscsi.discoveryauth.mutual_chap_peerusers')

await self.middleware.call(
'datastore.update', self._config.datastore, id_, new,
{'prefix': self._config.datastore_prefix}
)

await self.middleware.call('iscsi.discoveryauth.recalc_mutual_chap_alert', orig_peerusers)
await self._service_change('iscsitarget', 'reload')

return await self.get_instance(id_)
Expand All @@ -121,25 +127,34 @@ async def do_delete(self, audit_callback, id_):
audit_callback(_auth_summary(config))

if not await self.query([['tag', '=', config['tag']], ['id', '!=', id_]]):
usages = await self.is_in_use_by_portals_targets(id_)
# We are attempting to delete the last auth in a particular group (aka tag)
usages = await self.is_in_use(id_)
if usages['in_use']:
raise CallError(usages['usages'])

return await self.middleware.call(
orig_peerusers = await self.middleware.call('iscsi.discoveryauth.mutual_chap_peerusers')

result = await self.middleware.call(
'datastore.delete', self._config.datastore, id_
)
if orig_peerusers:
await self.middleware.call('iscsi.discoveryauth.recalc_mutual_chap_alert', orig_peerusers)

return result

@private
async def is_in_use_by_portals_targets(self, id_):
async def is_in_use(self, id_):
config = await self.get_instance(id_)
usages = []
portals = await self.middleware.call(
'iscsi.portal.query', [['discovery_authgroup', '=', config['tag']]], {'select': ['id']}
# Check discovery auth
discovery_auths = await self.middleware.call(
'iscsi.discoveryauth.query', [['authgroup', '=', config['tag']]], {'select': ['id']}
)
if portals:
if discovery_auths:
usages.append(
f'Authorized access of {id_} is being used by portal(s): {", ".join(str(p["id"]) for p in portals)}'
f'Authorized access of {id_} is being used by discovery auth(s): {", ".join(str(a["id"]) for a in discovery_auths)}'
)
# Check targets
groups = await self.middleware.call(
'datastore.query', 'services.iscsitargetgroups', [['iscsi_target_authgroup', '=', config['tag']]]
)
Expand Down
Loading

0 comments on commit 5447c45

Please sign in to comment.