diff --git a/adminsec/ldap.py b/adminsec/ldap.py index 26f8ac0..e2dd865 100644 --- a/adminsec/ldap.py +++ b/adminsec/ldap.py @@ -188,6 +188,7 @@ def get_user_info(self, username): "userAccountControl", "telephoneNumber", "uidNumber", + "cn", ], } diff --git a/adminsec/tasks.py b/adminsec/tasks.py index acd24f2..8374866 100644 --- a/adminsec/tasks.py +++ b/adminsec/tasks.py @@ -58,6 +58,8 @@ def _sync_ldap(write=False, verbose=False, ldapcon=None): first_name = userinfo.givenName last_name = userinfo.sn mail = userinfo.mail + name = userinfo.cn + display_name = userinfo.displayName disabled = True if userAccountControl: @@ -78,7 +80,12 @@ def _sync_ldap(write=False, verbose=False, ldapcon=None): if uid: user.uid = uid[0] - user.name = " ".join([user.first_name, user.last_name]) + if name: + user.name = name[0] + + if display_name: + user.display_name = display_name[0] + user.is_active = not disabled if user.hpcuser_user.exists(): diff --git a/adminsec/tests/test_tasks.py b/adminsec/tests/test_tasks.py index 3b569ee..45989a7 100644 --- a/adminsec/tests/test_tasks.py +++ b/adminsec/tests/test_tasks.py @@ -104,7 +104,7 @@ def setup_test_data_server1(connection): }, ) connection.strategy.add_entry( - "cn=user,ou=test," + AUTH_LDAP_USER_SEARCH_BASE, + "cn=Jane Joe,ou=test," + AUTH_LDAP_USER_SEARCH_BASE, { "objectclass": "person", "mail": USER_MAIL_INSTITUTE, @@ -126,7 +126,7 @@ def setup_test_data_server2(connection): }, ) connection.strategy.add_entry( - "cn=user,ou=test," + AUTH_LDAP2_USER_SEARCH_BASE, + "cn=John Doe,ou=test," + AUTH_LDAP2_USER_SEARCH_BASE, { "objectclass": "person", "mail": USER_MAIL_INSTITUTE2, diff --git a/adminsec/tests/test_views_api.py b/adminsec/tests/test_views_api.py index 7c8666d..9833f6b 100644 --- a/adminsec/tests/test_views_api.py +++ b/adminsec/tests/test_views_api.py @@ -8,7 +8,6 @@ from test_plus import TestCase from usersec.models import REQUEST_STATUS_ACTIVE -from usersec.serializers import HPC_ALUMNI_GROUP from usersec.tests.factories import ( HpcGroupCreateRequestFactory, HpcGroupFactory, @@ -627,126 +626,3 @@ def test_delete_fail(self): self.response_405() else: self.response_403() - - -class TestHpcAccessStatusApiView(ApiTestCase): - """Tests for the HpcAccessStatusApiView.""" - - def test_get_succeed(self): - """Test the GET method (staff users can do).""" - - expected = { - "hpc_users": [ - { - "uid": self.hpcuser_user.uid, - "email": self.hpcuser_user.user.email, - "full_name": "User Name", - "first_name": self.hpcuser_user.user.first_name, - "last_name": self.hpcuser_user.user.last_name, - "phone_number": None, - "primary_group": self.hpcuser_group.name, - "resources_requested": self.hpcuser_user.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_user.description, - "username": self.hpcuser_user.username, - "expiration": self.hpcuser_user.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "home_directory": self.hpcuser_user.home_directory, - "login_shell": self.hpcuser_user.login_shell, - } - ], - "hpc_groups": [ - { - "owner": None, - "delegate": None, - "resources_requested": self.hpcuser_group.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_group.description, - "name": self.hpcuser_group.name, - "folders": self.hpcuser_group.folders, - "expiration": self.hpcuser_group.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "gid": self.hpcuser_group.gid, - } - ], - "hpc_projects": [ - { - "gid": self.hpcuser_project.gid, - "group": self.hpcuser_group.name, - "delegate": None, - "resources_requested": self.hpcuser_project.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_project.description, - "name": self.hpcuser_project.name, - "folders": self.hpcuser_project.folders, - "expiration": self.hpcuser_project.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "members": [], - } - ], - } - for user in [self.user_staff, self.user_admin, self.user_hpcadmin]: - with self.login(user): - self.get("adminsec:api-hpcaccess-status") - self.response_200() - self.assertEqual(self.last_response.json(), expected) - - def test_get_succeed_alumni(self): - self.hpcuser_user.primary_group = None - self.hpcuser_user.save() - expected = { - "hpc_users": [ - { - "uid": self.hpcuser_user.uid, - "email": self.hpcuser_user.user.email, - "full_name": "User Name", - "first_name": self.hpcuser_user.user.first_name, - "last_name": self.hpcuser_user.user.last_name, - "phone_number": None, - "primary_group": HPC_ALUMNI_GROUP, - "resources_requested": self.hpcuser_user.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_user.description, - "username": self.hpcuser_user.username, - "expiration": self.hpcuser_user.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "home_directory": self.hpcuser_user.home_directory, - "login_shell": self.hpcuser_user.login_shell, - } - ], - "hpc_groups": [ - { - "owner": None, - "delegate": None, - "resources_requested": self.hpcuser_group.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_group.description, - "name": self.hpcuser_group.name, - "folders": self.hpcuser_group.folders, - "expiration": self.hpcuser_group.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "gid": self.hpcuser_group.gid, - } - ], - "hpc_projects": [ - { - "gid": self.hpcuser_project.gid, - "group": self.hpcuser_group.name, - "delegate": None, - "resources_requested": self.hpcuser_project.resources_requested, - "status": "INITIAL", - "description": self.hpcuser_project.description, - "name": self.hpcuser_project.name, - "folders": self.hpcuser_project.folders, - "expiration": self.hpcuser_project.expiration.strftime("%Y-%m-%dT%H:%M:%SZ"), - "members": [], - } - ], - } - for user in [self.user_staff, self.user_admin, self.user_hpcadmin]: - with self.login(user): - self.get("adminsec:api-hpcaccess-status") - self.response_200() - self.assertEqual(self.last_response.json(), expected) - - def test_get_fail(self): - """Test the GET method (non-staff cannot do).""" - for user in [self.user_user]: - with self.login(user): - self.get("adminsec:api-hpcaccess-status") - self.response_403() diff --git a/adminsec/urls.py b/adminsec/urls.py index ae3f276..61164ca 100644 --- a/adminsec/urls.py +++ b/adminsec/urls.py @@ -318,11 +318,6 @@ view=views_api.HpcProjectCreateRequestRetrieveUpdateApiView.as_view(), name="api-hpcprojectcreaterequest-retrieveupdate", ), - path( - "api/hpcaccess-status/", - view=views_api.HpcAccessStatusApiView.as_view(), - name="api-hpcaccess-status", - ), ] urlpatterns = urlpatterns_ui + urlpatterns_api diff --git a/adminsec/views_api.py b/adminsec/views_api.py index 0b73122..42323ec 100644 --- a/adminsec/views_api.py +++ b/adminsec/views_api.py @@ -5,7 +5,6 @@ from rest_framework.exceptions import ValidationError from rest_framework.generics import ( ListAPIView, - RetrieveAPIView, RetrieveUpdateAPIView, get_object_or_404, ) @@ -15,7 +14,6 @@ RE_FOLDER, RE_NAME, ) -from adminsec.models import HpcAccessStatus from adminsec.permissions_api import IsHpcAdminUser from hpcaccess.utils.rest_framework import CursorPagination from usersec.models import ( @@ -26,7 +24,6 @@ HpcUser, ) from usersec.serializers import ( - HpcAccessStatusSerializer, HpcGroupCreateRequestSerializer, HpcGroupSerializer, HpcProjectCreateRequestSerializer, @@ -218,18 +215,3 @@ def perform_update(self, serializer): raise ValidationError(errors) super().perform_update(serializer) - - -class HpcAccessStatusApiView(RetrieveAPIView): - """API view for listing all users.""" - - serializer_class = HpcAccessStatusSerializer - permission_classes = [IsAdminUser | IsHpcAdminUser] - - def get_object(self): - """Return the object to be used in the view.""" - return HpcAccessStatus( - hpc_users=HpcUser.objects.all(), - hpc_groups=HpcGroup.objects.all(), - hpc_projects=HpcProject.objects.all(), - ) diff --git a/usersec/serializers.py b/usersec/serializers.py index 0f84518..cd61803 100644 --- a/usersec/serializers.py +++ b/usersec/serializers.py @@ -103,36 +103,6 @@ class Meta: ] -class HpcUserStatusSerializer(HpcUserAbstractSerializer, serializers.ModelSerializer): - """Serializer for HpcUser model.""" - - primary_group = serializers.SerializerMethodField() - - def get_primary_group(self, obj): - if obj.primary_group is None: - return HPC_ALUMNI_GROUP - return obj.primary_group.name - - class Meta: - model = HpcUser - fields = [ - "uid", - "email", - "full_name", - "first_name", - "last_name", - "phone_number", - "primary_group", - "resources_requested", - "status", - "description", - "username", - "expiration", - "home_directory", - "login_shell", - ] - - class HpcUserVersionSerializer(HpcUserAbstractSerializer, serializers.ModelSerializer): """Serializer for HpcUserVersion model.""" @@ -200,26 +170,6 @@ class Meta: ] -class HpcGroupStatusSerializer(HpcGroupAbstractSerializer, serializers.ModelSerializer): - """Serializer for HpcGroup model.""" - - owner = serializers.SlugRelatedField(slug_field="username", read_only=True) - - class Meta: - model = HpcUser - fields = [ - "owner", - "delegate", - "resources_requested", - "status", - "description", - "name", - "folders", - "expiration", - "gid", - ] - - class HpcProjectAbstractSerializer(HpcObjectAbstractSerializer): """Common base class for HPC project serializers.""" @@ -276,29 +226,6 @@ class Meta: ] -class HpcProjectStatusSerializer(HpcProjectAbstractSerializer, serializers.ModelSerializer): - """Serializer for HpcProject model.""" - - group = serializers.SlugRelatedField(slug_field="name", read_only=True) - delegate = serializers.SlugRelatedField(slug_field="username", read_only=True) - members = serializers.SlugRelatedField(slug_field="username", many=True, read_only=True) - - class Meta: - model = HpcUser - fields = [ - "gid", - "group", - "delegate", - "resources_requested", - "status", - "description", - "name", - "folders", - "expiration", - "members", - ] - - class HpcRequestAbstractSerializer(HpcObjectAbstractSerializer): """Common base class for HPC request serializers.""" @@ -456,20 +383,3 @@ class Meta: "primary_group", "full_name", ] - - -class HpcAccessStatusSerializer(serializers.Serializer): - """Serializer for HpcAccessStatus model.""" - - hpc_users = serializers.SerializerMethodField() - hpc_groups = serializers.SerializerMethodField() - hpc_projects = serializers.SerializerMethodField() - - def get_hpc_users(self, obj): - return HpcUserStatusSerializer(obj.hpc_users, many=True).data - - def get_hpc_groups(self, obj): - return HpcGroupStatusSerializer(obj.hpc_groups, many=True).data - - def get_hpc_projects(self, obj): - return HpcProjectStatusSerializer(obj.hpc_projects, many=True).data diff --git a/usersec/tests/test_serializers.py b/usersec/tests/test_serializers.py index 7a325c8..eec44e1 100644 --- a/usersec/tests/test_serializers.py +++ b/usersec/tests/test_serializers.py @@ -6,12 +6,9 @@ from usersec.serializers import ( HpcGroupCreateRequestSerializer, HpcGroupSerializer, - HpcGroupStatusSerializer, HpcProjectCreateRequestSerializer, HpcProjectSerializer, - HpcProjectStatusSerializer, HpcUserSerializer, - HpcUserStatusSerializer, ) from usersec.tests.factories import ( HpcGroupCreateRequestFactory, @@ -106,73 +103,3 @@ def testSerializerExisting(self): result["group"] = "group_uuid_placeholder" result["name_requested"] = "name_requested_placeholder" self.assertMatchSnapshot(result) - - -@freeze_time(FROZEN_TIME) -class TestHpcUserStatusSerializer(ResetSequenceMixin, TestCaseSnap, TestCasePlus): - def setUp(self): - super().setUp() - self.hpc_user = HpcUserFactory() - - def testSerializerExisting(self): - serializer = HpcUserStatusSerializer(self.hpc_user) - result = dict(serializer.data) - result["email"] = "email_placeholder" - result["primary_group"] = "primary_group_name_placeholder" - result["phone_number"] = "phone_number_placeholder" - result["full_name"] = "name_placeholder" - result["first_name"] = "first_name_placeholder" - result["last_name"] = "last_name_placeholder" - self.assertMatchSnapshot(result) - - -@freeze_time(FROZEN_TIME) -class TestHpcGroupStatusSerializer(ResetSequenceMixin, TestCaseSnap, TestCasePlus): - def setUp(self): - super().setUp() - self.hpc_group = HpcGroupFactory() - - def testSerializerExisting(self): - serializer = HpcGroupStatusSerializer(self.hpc_group) - result = dict(serializer.data) - self.assertMatchSnapshot(result) - - -@freeze_time(FROZEN_TIME) -class TestHpcProjectStatusSerializer(ResetSequenceMixin, TestCaseSnap, TestCasePlus): - def setUp(self): - super().setUp() - self.hpc_project = HpcProjectFactory() - - def testSerializerExisting(self): - serializer = HpcProjectStatusSerializer(self.hpc_project) - result = dict(serializer.data) - result["group"] = "group_name_placeholder" - self.assertMatchSnapshot(result) - - -# TODO somehow the tests fail, but the serializer and API actually work -# @freeze_time(FROZEN_TIME) -# class TestHcpaccessStatus(ResetSequenceMixin, TestCaseSnap, TestCasePlus): -# def setUp(self): -# super().setUp() -# hpc_group = HpcGroupFactory() -# HpcProjectFactory(group=hpc_group) -# HpcUserFactory(primary_group=hpc_group) -# self.hpc_access_status = HpcAccessStatus( -# hpc_users=HpcUser.objects.all(), -# hpc_groups=HpcGroup.objects.all(), -# hpc_projects=HpcProject.objects.all(), -# ) - -# def testSerializerExisting(self): -# serializer = HpcAccessStatusSerializer(self.hpc_access_status) -# result = dict(serializer.data) -# result["hpc_users"][0]["email"] = "email_placeholder" -# result["hpc_users"][0]["primary_group"] = "primary_group_name_placeholder" -# result["hpc_users"][0]["phone_number"] = "phone_number_placeholder" -# result["hpc_users"][0]["full_name"] = "name_placeholder" -# result["hpc_users"][0]["first_name"] = "first_name_placeholder" -# result["hpc_users"][0]["last_name"] = "last_name_placeholder" -# result["hpc_users"][0]["uid"] = 2000 -# self.assertMatchSnapshot(result) diff --git a/utils/cli/hpc_access_cli/fs.py b/utils/cli/hpc_access_cli/fs.py index 497f8e6..3290fea 100644 --- a/utils/cli/hpc_access_cli/fs.py +++ b/utils/cli/hpc_access_cli/fs.py @@ -17,6 +17,206 @@ console_err = Console(file=sys.stderr) +FS_USER_OPS = r""" +#!/bin/bash + +USERNAME=%(username)s +HOME=%(folder_home)s +WORK=%(folder_work)s +SCRATCH=%(folder_scratch)s +GROUP_WORK=%(folder_group_work)s +GROUP_SCRATCH=%(folder_group_scratch)s + +# Check that user and group exist +if ! getent passwd $USERNAME >/dev/null; then + >&2 echo "User $USERNAME does not exist." + exit 1 +fi + +# Check that the group folders exist +for dir in $GROUP_WORK $GROUP_SCRATCH; do + if ! [[ -d "$dir" ]]; then + >&2 echo "$dir directory does not exist." + exit 1 + fi +done + +# Check that the directories do not exist yet +for dir in $HOME $WORK $SCRATCH; do + if [[ -d $dir ]]; then + >&2 echo "Directory $dir already exists." + exit 1 + fi +done + +mkdir $HOME +mkdir $WORK +mkdir $SCRATCH + +# Create utility dirs +mkdir $WORK/{R,ondemand,.apptainer,.local} +mkdir $SCRATCH/.cache + +# Set quota +setfattr -n ceph.quota.max_bytes -v $((1*(1024**3))) $HOME + +# Transfer skel directory +rsync -av /etc/skel.bih/. $HOME/. +mkdir -p $HOME/.ssh +touch $HOME/.ssh/authorized_keys +chmod -R u=rwX,go= $HOME +ssh-keygen -t ed25519 -C "$USERNAME on BIH HPC" -f $HOME/.ssh/id_ed25519 -N "" 2>/dev/null +cat $HOME/.ssh/id_ed25519.pub >> $HOME/.ssh/authorized_keys + +# Set remaining owners and permissions +chown -R $USERNAME:$GROUP $HOME +chown -R $USERNAME:$GROUP $WORK +chown -R $USERNAME:$GROUP $SCRATCH +chmod 700 $WORK +chmod 700 $SCRATCH + +echo "Creating symlinks..." +ln -s $WORK $HOME/work +ln -s $SCRATCH $HOME/scratch +ln -s work/R $HOME/R +ln -s work/ondemand $HOME/ondemand +ln -s work/.local $HOME/.local +ln -s work/.apptainer $HOME/.apptainer +ln -s scratch/.cache $HOME/.cache +""" + + +FS_GROUP_OPS = r""" +#!/bin/bash + +OWNER=%(owner)s +GROUP=%(group)s +QUOTA1=%(quota1)s +QUOTA2=%(quota2)s +WORK=%(folder_work)s +SCRATCH=%(folder_scratch)s +TIER2=%(folder_unmirrored)s + +# Check if group exists +if ! getent group $GROUP >/dev/null; then + >&2 echo "Group $GROUP does not exist." + exit 1 +fi + +# Check if quotas are valid +if ! [[ "$QUOTA1" =~ ^[1-9]+$ || "$QUOTA2" =~ ^[1-9]+$ ]]; then + >&2 echo "Quotas $QUOTA1 and $QUOTA2 are not positive integers." + exit 1 +fi + +# Check if owner exists +if [[ $OWNER == "" ]] || ! getent passwd $OWNER >/dev/null; then + >&2 echo "User $OWNER does not exist." + exit 1 +fi + +# Check that the directories do not exist yet +for dir in $WORK $SCRATCH $TIER2; do + if [[ -d $dir ]]; then + >&2 echo "Directory $dir already exists." + exit 1 + fi +done + +mkdir -p $WORK/users +mkdir -p $SCRATCH/users +mkdir $TIER2 +chown -R $OWNER:$GROUP $WORK +chown -R $OWNER:$GROUP $SCRATCH +chown -R $OWNER:$GROUP $TIER2 +chmod 770 $WORK +chmod 770 $SCRATCH +chmod 770 $TIER2 +chmod 750 $WORK/users +chmod 750 $SCRATCH/users + +# Set quotas +setfattr -n ceph.quota.max_bytes -v $(($QUOTA1*(1024**4))) $WORK +setfattr -n ceph.quota.max_bytes -v $(($QUOTA1*10*(1024**4))) $SCRATCH +setfattr -n ceph.quota.max_bytes -v $(($QUOTA2*(1024**4))) $TIER2 +""" + + +FS_PROJECT_OPS = r""" +#!/bin/bash + +GROUP=%(group)s +QUOTA1=%(quota1)s +QUOTA2=%(quota2)s +OWNER=%(owner)s +WORK=%(folder_work)s +SCRATCH=%(folder_scratch)s +TIER2=%(folder_unmirrored)s + +# Check if group exists +if ! getent group $GROUP >/dev/null; then + >&2 echo "Group $GROUP does not exist." + exit 1 +fi + +# Check if quotas are valid +if ! [[ "$QUOTA1" =~ ^[0-9]+$ || "$QUOTA2" =~ ^[0-9]+$ ]]; then + >&2 echo "Quotas $QUOTA1 and $QUOTA2 are not integers." + exit 1 +fi + +# Check if owner exists +if [[ $OWNER == "" ]] || ! getent passwd $OWNER >/dev/null; then + >&2 echo "User $OWNER does not exist." + exit 1 +fi + +# Tier 1 +if [[ "$QUOTA1" -ne "0" ]]; then + # Check that the directories do not exist yet + for dir in $WORK $SCRATCH; do + if [[ -d $dir ]]; then + >&2 echo "Directory $dir already exists." + exit 1 + fi + done + + echo "Creating Tier 1 location with $QUOTA1 TB quota." + mkdir $WORK + mkdir $SCRATCH + chown -R $OWNER:$GROUP $WORK + chown -R $OWNER:$GROUP $SCRATCH + chmod -R 2770 $WORK + chmod -R 2770 $SCRATCH + + # Set quotas + setfattr -n ceph.quota.max_bytes -v $(($QUOTA1*(1024**4))) $WORK + setfattr -n ceph.quota.max_bytes -v $((10*(1024**4))) $SCRATCH + + ln -s $SCRATCH $WORK/scratch +fi + +# Tier 2 +if [[ "$QUOTA2" -ne "0" ]]; then + # Check that the directories do not exist yet + for dir in $TIER2; do + if [[ -d $dir ]]; then + >&2 echo "Directory $dir already exists." + exit 1 + fi + done + + echo "Creating Tier 2 location with $QUOTA2 TB quota." + mkdir $TIER2 + chown -R $OWNER:$GROUP $TIER2 + chmod -R 2770 $TIER2 + + # Set quota + setfattr -n ceph.quota.max_bytes -v $(($QUOTA2*(1024**4))) $TIER2 +fi +""" + + def get_extended_attribute(path: str, attr_name: str) -> str: """Get the value of an extended attribute.""" try: diff --git a/utils/cli/hpc_access_cli/ldap.py b/utils/cli/hpc_access_cli/ldap.py index 349b1d5..ed77688 100644 --- a/utils/cli/hpc_access_cli/ldap.py +++ b/utils/cli/hpc_access_cli/ldap.py @@ -10,7 +10,7 @@ from hpc_access_cli.config import LdapSettings from hpc_access_cli.models import ( LOGIN_SHELL_DISABLED, - Gecos, + # Gecos, LdapGroup, LdapGroupOp, LdapUser, @@ -80,18 +80,19 @@ def load_users(self) -> List[LdapUser]: "uidNumber", "gidNumber", "homeDirectory", - "gecos", + # "gecos", "loginShell", "mail", "displayName", "sshPublicKey", + "telephoneNumber", ], ): raise Exception("Failed to search for users.") result = [] for entry in self.connection.entries: - gecos_str = attribute_as_str(entry.gecos) - gecos = Gecos.from_string(gecos_str) if gecos_str else None + # gecos_str = attribute_as_str(entry.gecos) + # gecos = Gecos.from_string(gecos_str) if gecos_str else None uid_str = attribute_as_str(entry.uidNumber) uid_number = int(uid_str) if uid_str else None if not uid_number: @@ -108,6 +109,7 @@ def load_users(self) -> List[LdapUser]: raise ValueError(f"Missing LDAP attribute uid for {entry.entry_dn}") sn = attribute_as_str(entry.sn) given_name = attribute_as_str(entry.givenName) + display_name = attribute_as_str(entry.displayName) home_directory = attribute_as_str(entry.homeDirectory) if not home_directory: raise ValueError(f"Missing LDAP attribute homeDirectory for {entry.entry_dn}") @@ -122,12 +124,14 @@ def load_users(self) -> List[LdapUser]: sn=sn, mail=attribute_as_str(entry.mail), given_name=given_name, + display_name=display_name, uid_number=uid_number, gid_number=gid_number, home_directory=home_directory, login_shell=login_shell, - gecos=gecos, - ssh_public_key=attribute_list_as_str_list(entry.sshPublicKey), + telephone_number=attribute_as_str(entry.telephoneNumber), + # gecos=None, + # ssh_public_key=attribute_list_as_str_list(entry.sshPublicKey), ) ) return result @@ -147,6 +151,11 @@ def _user_op_create(self, user: LdapUser, dry_run: bool): "uid": user.uid, "uidNumber": user.uid_number, "homeDirectory": user.home_directory, + "mail": user.mail, + "telephoneNumber": user.telephone_number, + "loginShell": user.login_shell, + "gidNumber": user.gid_number, + "displayName": user.display_name, } if user.sn: user_data["sn"] = user.sn @@ -172,8 +181,8 @@ def _user_op_disable(self, user: LdapUser, dry_run: bool): "objectclass", "uid", "uidNumber", - "telephoneNumber", "mail", + "telephoneNumber", "displayName", "sshPublicKey", "loginShell", @@ -208,8 +217,8 @@ def _user_op_update( "objectclass", "uid", "uidNumber", - "telephoneNumber", "mail", + "telephoneNumber", "displayName", "sshPublicKey", "loginShell", @@ -226,10 +235,10 @@ def _user_op_update( applied_diff = {} for key, value in diff.items(): key = humps.camelize(key) - if key == "gecos": - gecos: Gecos = value or Gecos() # type: ignore - applied_diff[key] = Gecos.model_validate(gecos).to_string() - elif key == "sshPublicKey": + # if key == "gecos": + # gecos: Gecos = value or Gecos() # type: ignore + # applied_diff[key] = Gecos.model_validate(gecos).to_string() + if key == "sshPublicKey": # We only support clearing this list for now which is fine as the # SSH keys live in the upstream ADs only. applied_diff[key] = [(ldap3.MODIFY_DELETE, x) for x in writable[key]] @@ -273,7 +282,7 @@ def load_groups(self) -> List[LdapGroup]: raise ValueError(f"Missing LDAP attribute gidNumber for {entry.entry_dn}") owner_dn = attribute_as_str(entry["bih-groupOwnerDN"]) delegate_dns = attribute_list_as_str_list(entry["bih-groupDelegateDNs"]) - member_uids = attribute_list_as_str_list(entry.memberUid) + member_uids = sorted(attribute_list_as_str_list(entry.memberUid)) result.append( LdapGroup( dn=entry.entry_dn, diff --git a/utils/cli/hpc_access_cli/main.py b/utils/cli/hpc_access_cli/main.py index f217599..24489df 100644 --- a/utils/cli/hpc_access_cli/main.py +++ b/utils/cli/hpc_access_cli/main.py @@ -1,4 +1,3 @@ -import os import sys from typing import List @@ -9,18 +8,17 @@ from hpc_access_cli.config import load_settings from hpc_access_cli.constants import ENTITIES, ENTITY_USERS -from hpc_access_cli.fs import FsResourceManager -from hpc_access_cli.ldap import LdapConnection +from hpc_access_cli.fs import FS_GROUP_OPS, FS_PROJECT_OPS, FS_USER_OPS from hpc_access_cli.models import StateOperation from hpc_access_cli.states import ( TargetStateBuilder, TargetStateComparison, convert_to_hpcaccess_state, - convert_to_hpcaccess_state_v2, deploy_hpcaccess_state, fs_validation, gather_hpcaccess_state, gather_system_state, + user_dn, ) #: The typer application object to use. @@ -80,20 +78,6 @@ def dump_data( console_out.print_json(data=hpcaccess_state.model_dump(mode="json")) -@app.command("state-dump-v2") -def dump_data_v2( - config_path: Annotated[ - str, typer.Option(..., help="path to configuration file") - ] = "/etc/hpc-access-cli/config.json", -): - """dump system state as hpc-access state""" - settings = load_settings(config_path) - console_err.print_json(data=settings.model_dump(mode="json")) - system_state = gather_system_state(settings) - hpcaccess_state = convert_to_hpcaccess_state_v2(system_state) - console_out.print_json(data=hpcaccess_state.model_dump(mode="json")) - - @app.command("state-sync") def sync_data( config_path: Annotated[ @@ -124,22 +108,134 @@ def sync_data( ) # console_err.print_json(data=settings.model_dump(mode="json")) src_state = gather_system_state(settings) - dst_builder = TargetStateBuilder(settings.hpc_access, src_state) + hpcaccess_state = gather_hpcaccess_state(settings.hpc_access) + dst_builder = TargetStateBuilder(hpcaccess_state, src_state) dst_state = dst_builder.run() - comparison = TargetStateComparison(settings.hpc_access, src_state, dst_state) + comparison = TargetStateComparison(src_state, dst_state) operations = comparison.run() + group_by_gid = {g.gid: g for g in hpcaccess_state.hpc_groups.values()} + user_by_uuid = {u.uuid: u for u in hpcaccess_state.hpc_users.values()} + owner_by_dn = { + user_dn(user_by_uuid[g.owner]): g.owner for g in hpcaccess_state.hpc_groups.values() + } # console_err.print_json(data=operations.model_dump(mode="json")) - connection = LdapConnection(settings.ldap_hpc) - console_err.log(f"applying LDAP group operations now, dry_run={dry_run}") - for group_op in operations.ldap_group_ops: - connection.apply_group_op(group_op, dry_run) - console_err.log(f"applying LDAP user operations now, dry_run={dry_run}") - for user_op in operations.ldap_user_ops: - connection.apply_user_op(user_op, dry_run) - console_err.log(f"applying file system operations now, dry_run={dry_run}") - fs_mgr = FsResourceManager(prefix="/data/sshfs" if os.environ.get("DEBUG", "0") == "1" else "") - for fs_op in operations.fs_ops: - fs_mgr.apply_fs_op(fs_op, dry_run) + with open("ldap_user_ops.ldif", "w") as fh_ldap_user_ops: + for user_op in operations.ldap_user_ops: + if user_op.operation == StateOperation.CREATE: + console_err.log(f"create user {user_op.user.dn}") + fh_ldap_user_ops.write(f"dn: {user_op.user.dn}\n") + fh_ldap_user_ops.write("changetype: add\n") + fh_ldap_user_ops.write("objectClass: inetOrgPerson\n") + fh_ldap_user_ops.write("objectClass: posixAccount\n") + fh_ldap_user_ops.write("objectClass: ldapPublicKey\n") + fh_ldap_user_ops.write("objectClass: bih-expireDates\n") + fh_ldap_user_ops.write("objectClass: top\n") + fh_ldap_user_ops.write(f"cn: {user_op.user.cn}\n") + fh_ldap_user_ops.write(f"gidNumber: {user_op.user.gid_number}\n") + fh_ldap_user_ops.write(f"homeDirectory: {user_op.user.home_directory}\n") + fh_ldap_user_ops.write(f"sn: {user_op.user.sn}\n") + fh_ldap_user_ops.write(f"uid: {user_op.user.uid}\n") + fh_ldap_user_ops.write(f"uidNumber: {user_op.user.uid_number}\n") + if user_op.user.given_name: + fh_ldap_user_ops.write(f"givenName: {user_op.user.given_name}\n") + if user_op.user.login_shell: + fh_ldap_user_ops.write(f"loginShell: {user_op.user.login_shell}\n") + if user_op.user.mail: + fh_ldap_user_ops.write(f"mail: {user_op.user.mail}\n") + if user_op.user.telephone_number: + fh_ldap_user_ops.write(f"telephoneNumber: {user_op.user.telephone_number}\n") + fh_ldap_user_ops.write("\n") + + group_folders = group_by_gid[user_op.user.gid_number].folders + users_folder = f"users/{user_op.user.uid}" + + with open(f"fs_user_ops_{user_op.user.uid}.sh", "w") as fh_fs_user_ops: + fh_fs_user_ops.write( + FS_USER_OPS + % { + "username": user_op.user.uid, + "folder_home": user_op.user.home_directory, + "folder_work": f"{group_folders.tier1_work}/{users_folder}", + "folder_scratch": f"{group_folders.tier1_scratch}/{users_folder}", + "folder_group_work": group_folders.tier1_work, + "folder_group_scratch": group_folders.tier1_scratch, + } + ) + + elif user_op.operation == StateOperation.UPDATE: + console_err.log(f"update user {user_op.user.dn}") + fh_ldap_user_ops.write(f"dn: {user_op.user.dn}\n") + fh_ldap_user_ops.write("changetype: modify\n") + for i, (key, value) in enumerate(user_op.diff.items(), 1): + if not value: + fh_ldap_user_ops.write(f"delete: {key}\n") + else: + fh_ldap_user_ops.write(f"replace: {key}\n") + fh_ldap_user_ops.write(f"{key}: {value}\n") + if i < len(user_op.diff): + fh_ldap_user_ops.write("-\n") + fh_ldap_user_ops.write("\n") + + elif user_op.operation == StateOperation.DISABLE: + console_err.log(f"disable user {user_op.user.dn}") + fh_ldap_user_ops.write(f"dn: {user_op.user.dn}\n") + fh_ldap_user_ops.write("changetype: modify\n") + fh_ldap_user_ops.write("replace: login_shell\n") + fh_ldap_user_ops.write("login_shell: /usr/sbin/nologin\n\n") + + with open("ldap_group_ops.ldif", "w") as fh_ldap_group_ops: + for group_op in operations.ldap_group_ops: + if group_op.operation == StateOperation.CREATE: + console_err.log(f"create group {group_op.group.dn}") + fh_ldap_group_ops.write(f"dn: {group_op.group.dn}\n") + fh_ldap_group_ops.write("changetype: add\n") + fh_ldap_group_ops.write("objectClass: groupOfNames\n") + fh_ldap_group_ops.write("objectClass: top\n") + fh_ldap_group_ops.write(f"cn: {group_op.group.cn}\n") + for member in group_op.group.member_uids: + fh_ldap_group_ops.write(f"member: {member}\n") + fh_ldap_group_ops.write("\n") + FS_OPS = FS_PROJECT_OPS if group_op.group.cn.startswith("hpc-prj") else FS_GROUP_OPS + group = group_by_gid[group_op.group.gid_number] + with open(f"fs_group_ops_{group_op.group.dn}.sh", "w") as fh_fs_group_ops: + fh_fs_group_ops.write( + FS_OPS + % { + "owner": owner_by_dn(group_op.group.owner_dn), + "group": group_op.group.cn, + "quota1": group.resources_requested.tier1_work, + "quota2": group.resources_requested.tier1_scratch, + "folder_work": group.folders.tier1_work, + "folder_scratch": group.folders.tier1_scratch, + "folder_unmirrored": group.folders.tier2_unmirrored, + } + ) + + elif group_op.operation == StateOperation.UPDATE: + console_err.log(f"update group {group_op.group.dn}") + fh_ldap_group_ops.write(f"dn: {group_op.group.dn}\n") + fh_ldap_group_ops.write("changetype: modify\n") + for i, (key, value) in enumerate(group_op.diff.items(), 1): + if not value: + fh_ldap_group_ops.write(f"delete: {key}\n") + else: + fh_ldap_group_ops.write(f"replace: {key}\n") + fh_ldap_group_ops.write(f"{key}: {value}\n") + if i < len(group_op.diff): + fh_ldap_group_ops.write("-\n") + fh_ldap_group_ops.write("\n") + + # connection = LdapConnection(settings.ldap_hpc) + # console_err.log(f"applying LDAP group operations now, dry_run={dry_run}") + # for group_op in operations.ldap_group_ops: + # connection.apply_group_op(group_op, dry_run) + # console_err.log(f"applying LDAP user operations now, dry_run={dry_run}") + # for user_op in operations.ldap_user_ops: + # connection.apply_user_op(user_op, dry_run) + # console_err.log(f"applying file system operations now, dry_run={dry_run}") + # fs_mgr = FsResourceManager("") + # for fs_op in operations.fs_ops: + # fs_mgr.apply_fs_op(fs_op, dry_run) @app.command("storage-usage-sync") diff --git a/utils/cli/hpc_access_cli/models.py b/utils/cli/hpc_access_cli/models.py index 14cf86f..36105f0 100644 --- a/utils/cli/hpc_access_cli/models.py +++ b/utils/cli/hpc_access_cli/models.py @@ -97,45 +97,45 @@ def from_path(path: str) -> "FsDirectory": ) -class Gecos(BaseModel): - """GECOS information about a user.""" - - #: The full name of the user. - full_name: Optional[str] = None - #: The office location of the user. - office_location: Optional[str] = None - #: The office phone number of the user. - office_phone: Optional[str] = None - #: The home phone number of the user. - home_phone: Optional[str] = None - #: The other information about the user. - other: Optional[str] = None - - def to_string(self): - """Convert the GECOS information to a GECOS string.""" - return ",".join( - [ - self.full_name if self.full_name else "", - self.office_location if self.office_location else "", - self.office_phone if self.office_phone else "", - self.home_phone if self.home_phone else "", - self.other if self.other else "", - ] - ) - - @staticmethod - def from_string(gecos: str) -> "Gecos": - """Create a new instance from a GECOS string.""" - parts = gecos.split(",", 4) - if len(parts) < 5: - parts.extend([""] * (5 - len(parts))) - return Gecos( - full_name=parts[0] if parts[0] != "None" else None, - office_location=parts[1] if parts[1] != "None" else None, - office_phone=parts[2] if parts[2] != "None" else None, - home_phone=parts[3] if parts[3] != "None" else None, - other=parts[4] if parts[4] != "None" else None, - ) +# class Gecos(BaseModel): +# """GECOS information about a user.""" + +# #: The full name of the user. +# full_name: Optional[str] = None +# #: The office location of the user. +# office_location: Optional[str] = None +# #: The office phone number of the user. +# office_phone: Optional[str] = None +# #: The home phone number of the user. +# home_phone: Optional[str] = None +# #: The other information about the user. +# other: Optional[str] = None + +# def to_string(self): +# """Convert the GECOS information to a GECOS string.""" +# return ",".join( +# [ +# self.full_name if self.full_name else "", +# self.office_location if self.office_location else "", +# self.office_phone if self.office_phone else "", +# self.home_phone if self.home_phone else "", +# self.other if self.other else "", +# ] +# ) + +# @staticmethod +# def from_string(gecos: str) -> "Gecos": +# """Create a new instance from a GECOS string.""" +# parts = gecos.split(",", 4) +# if len(parts) < 5: +# parts.extend([""] * (5 - len(parts))) +# return Gecos( +# full_name=parts[0] if parts[0] != "None" else None, +# office_location=parts[1] if parts[1] != "None" else None, +# office_phone=parts[2] if parts[2] != "None" else None, +# home_phone=parts[3] if parts[3] != "None" else None, +# other=parts[4] if parts[4] != "None" else None, +# ) class LdapUser(BaseModel): @@ -153,6 +153,8 @@ class LdapUser(BaseModel): sn: Optional[str] #: The user's given name. given_name: Optional[str] + #: The user's display name. + display_name: Optional[str] #: The numeric user ID. uid_number: int #: The primary group of the user. @@ -161,10 +163,12 @@ class LdapUser(BaseModel): home_directory: str #: The login shell of the user. login_shell: str - #: The GECOS information of the user. - gecos: Optional[Gecos] - #: Public SSH keys. - ssh_public_key: List[str] + # #: The GECOS information of the user. + # gecos: Optional[Gecos] + # #: Public SSH keys. + # ssh_public_key: List[str] + #: Telephone number. + telephone_number: Optional[str] class LdapGroup(BaseModel): @@ -275,37 +279,6 @@ class HpcUser(BaseModel): current_version: int -class HpcUserV2(BaseModel): - """A user as read from the hpc-access API.""" - - #: The UUID of the primary ``HpcGroup``. - primary_group: Optional[str] - #: Description of the record. - description: Optional[str] - #: The user's email address. - email: Optional[str] - #: The full name of the user. - full_name: str - #: The first name fo the user. - first_name: Optional[str] - #: The last name of the user. - last_name: Optional[str] - #: The office phone number of the user. - phone_number: Optional[str] - #: The requested resources. - resources_requested: Optional[ResourceDataUser] - #: The status of the record. - status: Status - #: The POSIX UID of the user. - uid: int - #: The username of the record. - username: str - #: The home directory. - home_directory: str - #: The login shell - login_shell: str - - class HpcGroup(BaseModel): """A group as read from the hpc-access API.""" @@ -335,27 +308,6 @@ class HpcGroup(BaseModel): current_version: int -class HpcGroupV2(BaseModel): - """A group as read from the hpc-access API.""" - - #: The owning ``HpcUser``. - owner: str - #: Description of the record. - description: Optional[str] - #: The delegate. - delegate: Optional[str] - #: The requested resources. - resources_requested: Optional[ResourceData] - #: The status of the record. - status: Status - #: The POSIX GID of the corresponding Unix group. - gid: Optional[int] - #: The name of the record. - name: str - #: The folders of the group. - folders: GroupFolders - - class HpcProject(BaseModel): """A project as read from the hpc-access API.""" @@ -387,29 +339,6 @@ class HpcProject(BaseModel): members: List[UUID] -class HpcProjectV2(BaseModel): - """A project as read from the hpc-access API.""" - - #: The owning ``HpcGroup``, owner of group is owner of project. - group: Optional[str] - #: Description of the record. - description: Optional[str] - #: The delegate for the project. - delegate: Optional[str] - #: The requested resources. - resources_requested: Optional[ResourceData] - #: The status of the record. - status: Status - #: The POSIX GID of the corresponding Unix group. - gid: Optional[int] - #: The name of the record. - name: str - #: The folders of the group. - folders: GroupFolders - #: The project's member user UUIDs. - members: List[str] - - class SystemState(BaseModel): """System state retrieved from LDAP and file system.""" @@ -429,14 +358,6 @@ class HpcaccessState(BaseModel): hpc_projects: Dict[UUID, HpcProject] -class HpcaccessStateV2(BaseModel): - """State as loaded from hpc-access.""" - - hpc_users: List[HpcUserV2] - hpc_groups: List[HpcGroupV2] - hpc_projects: List[HpcProjectV2] - - @enum.unique class StateOperation(enum.Enum): """Operation to perform on the state.""" diff --git a/utils/cli/hpc_access_cli/states.py b/utils/cli/hpc_access_cli/states.py index fc96a4c..3ab78b5 100644 --- a/utils/cli/hpc_access_cli/states.py +++ b/utils/cli/hpc_access_cli/states.py @@ -37,16 +37,12 @@ LOGIN_SHELL_DISABLED, FsDirectory, FsDirectoryOp, - Gecos, + # Gecos, GroupFolders, HpcaccessState, - HpcaccessStateV2, HpcGroup, - HpcGroupV2, HpcProject, - HpcProjectV2, HpcUser, - HpcUserV2, LdapGroup, LdapGroupOp, LdapUser, @@ -121,9 +117,9 @@ class TargetStateBuilder: from hpc-access. """ - def __init__(self, settings: HpcaccessSettings, system_state: SystemState): - #: The settings to use. - self.settings = settings + def __init__(self, hpcaccess_state: HpcaccessState, system_state: SystemState): + #: The hpc-access state to use. + self.hpcaccess_state = hpcaccess_state #: The current system state, used for determining next group id. self.system_state = system_state #: The next gid. @@ -137,24 +133,19 @@ def _get_next_gid(self, system_state: SystemState) -> int: return max(gids) + 1 if gids else 1000 def run(self) -> SystemState: - """Run the builder.""" - hpcaccess_state = gather_hpcaccess_state(self.settings) - return self._build(hpcaccess_state) - - def _build(self, hpcaccess_state: HpcaccessState) -> SystemState: """Build the target state.""" # IMPORANT: Note that order matters here! First, we must create # LDAP groups so we have the Unix GIDs when users are considered. - ldap_groups = self._build_ldap_groups(hpcaccess_state) - ldap_users = self._build_ldap_users(hpcaccess_state) + ldap_groups = self._build_ldap_groups(self.hpcaccess_state) + ldap_users = self._build_ldap_users(self.hpcaccess_state) # build hpc-users group ldap_groups["hpc-users"] = LdapGroup( dn="cn=hpc-users,ou=Groups,dc=hpc,dc=bihealth,dc=org", cn="hpc-users", gid_number=HPC_USERS_GID, - description="users allowed to login (active+have group)", owner_dn=None, delegate_dns=[], + description="users allowed to login (active+have group)", member_uids=[ u.uid for u in ldap_users.values() @@ -164,7 +155,7 @@ def _build(self, hpcaccess_state: HpcaccessState) -> SystemState: return SystemState( ldap_users=ldap_users, ldap_groups=ldap_groups, - fs_directories=self._build_fs_directories(hpcaccess_state), + fs_directories=self._build_fs_directories(self.hpcaccess_state), ) def _build_fs_directories(self, hpcaccess_state: HpcaccessState) -> Dict[str, FsDirectory]: @@ -313,12 +304,13 @@ def _build_ldap_users(self, hpcaccess_state: HpcaccessState) -> Dict[str, LdapUs """Build the LDAP users from the hpc-access state.""" result = {} for user in hpcaccess_state.hpc_users.values(): - gecos = Gecos( - full_name=user.full_name, - office_location=None, - office_phone=user.phone_number, - other=None, - ) + # gecos = Gecos( + # full_name=user.full_name, + # office_location=None, + # office_phone=user.phone_number, + # home_phone=None, + # other=None, + # ) if user.primary_group: hpc_group = hpcaccess_state.hpc_groups[user.primary_group] group_gid = hpc_group.gid or HPC_ALUMNIS_GID @@ -329,17 +321,17 @@ def _build_ldap_users(self, hpcaccess_state: HpcaccessState) -> Dict[str, LdapUs cn=user.full_name, sn=user.last_name, given_name=user.first_name, + display_name=user.display_name, uid=user.username, mail=user.email, - gecos=gecos, + # gecos=None, uid_number=user.uid, gid_number=group_gid, - # user.home_directory - home_directory=f"{BASE_PATH_TIER1}/home/users/{user.username}", - # user.login_shell - login_shell="/usr/bin/bash", + home_directory=user.home_directory, + login_shell=user.login_shell, + telephone_number=user.phone_number, # SSH keys are managed via upstream LDAP. - ssh_public_key=[], + # ssh_public_key=[], ) return result @@ -387,7 +379,7 @@ def _build_ldap_groups(self, state: HpcaccessState) -> Dict[str, LdapGroup]: description=project.description, owner_dn=owner_dn, delegate_dns=[user_dn(delegate)] if delegate else [], - member_uids=[], + member_uids=sorted([state.hpc_users[m].username for m in project.members]), ) return result @@ -516,8 +508,9 @@ def build_hpcuser(u: LdapUser, quotas: Dict[str, str]) -> HpcUser: full_name=u.cn, first_name=u.given_name, last_name=u.sn, + display_name=u.display_name, email=u.mail, - phone_number=u.gecos.office_phone if u.gecos else None, + phone_number=u.telephone_number, resources_requested=ResourceDataUser(**quotas), resources_used=ResourceDataUser( tier1_home=0, @@ -578,6 +571,9 @@ def build_hpcproject(p: LdapGroup, quotas: Dict[str, str]) -> Optional[HpcProjec group = None else: group = group_uuids[group_by_gid_number[gid_number].cn] + owner_uuid = user_uuids[user_by_dn[group_by_gid_number[gid_number].owner_dn].uid] + if owner_uuid not in members: + members.append(owner_uuid) return HpcProject( uuid=group_uuids[p.cn], name=name, @@ -631,156 +627,6 @@ def build_hpcproject(p: LdapGroup, quotas: Dict[str, str]) -> Optional[HpcProjec ) -def convert_to_hpcaccess_state_v2(system_state: SystemState) -> HpcaccessStateV2: - """Convert hpc-access to system state. - - Note that this will make up the UUIDs. - """ - # create UUID mapping from user/groupnames - user_by_uid = {u.uid: u for u in system_state.ldap_users.values()} - user_by_dn = {u.dn: u for u in system_state.ldap_users.values()} - group_by_name = {strip_prefix(g.cn): g for g in system_state.ldap_groups.values()} - group_by_gid_number = {g.gid_number: g for g in system_state.ldap_groups.values()} - group_by_owner_dn: Dict[str, LdapGroup] = {} - for g in system_state.ldap_groups.values(): - if g.owner_dn: - group_by_owner_dn[user_by_dn[g.owner_dn].dn] = g - user_quotas: Dict[str, ResourceDataUser] = {} - group_quotas: Dict[str, ResourceData] = {} - for fs_data in system_state.fs_directories.values(): - try: - entity, name, resource = fs_validation(fs_data) - except ValueError as e: - console_err.log(f"WARNING: {e}") - continue - - quota_bytes = fs_data.quota_bytes if fs_data.quota_bytes is not None else 0 - - if entity == ENTITY_USERS: - if name not in user_by_uid: - console_err.log(f"WARNING: user {name} not found") - continue - if name not in user_quotas: - user_quotas[name] = {} - user_quotas[name][resource] = quota_bytes / 1024**3 - elif entity in (ENTITY_GROUPS, ENTITY_PROJECTS): - if name not in group_by_name: - console_err.log(f"WARNING: group {name} not found") - continue - if name not in group_quotas: - group_quotas[name] = {} - group_quotas[name][resource] = quota_bytes / 1024**4 - - def build_hpcuser(u: LdapUser, quotas: Dict[str, str]) -> HpcUserV2: - if u.login_shell != LOGIN_SHELL_DISABLED: - status = Status.ACTIVE - else: - status = Status.EXPIRED - if u.gid_number and u.gid_number in group_by_gid_number: - primary_group = group_by_gid_number[u.gid_number].cn - else: - primary_group = None - if not primary_group.startswith(POSIX_AG_PREFIX) and not primary_group == "hpc-alumnis": - console_err.log(f"User belongs to group that is not a group ({primary_group}, {u.uid})") - return HpcUserV2( - primary_group=strip_prefix(primary_group, prefix=POSIX_AG_PREFIX), - description=None, - full_name=u.cn, - first_name=u.given_name, - last_name=u.sn, - email=u.mail, - phone_number=u.gecos.office_phone if u.gecos else None, - resources_requested=ResourceDataUser(**quotas), - status=status, - uid=u.uid_number, - username=u.uid, - home_directory=u.home_directory, - login_shell=u.login_shell, - ) - - def build_hpcgroup(g: LdapGroup, quotas: Dict[str, str]) -> Optional[HpcGroupV2]: - name = strip_prefix(g.cn, POSIX_AG_PREFIX) - if not g.owner_dn: - console_err.log(f"no owner DN for {g.cn}, skipping") - return - return HpcGroupV2( - name=name, - description=g.description, - owner=user_by_dn[g.owner_dn].uid, - delegate=user_by_dn[g.delegate_dns[0]].uid if g.delegate_dns else None, - resources_requested=ResourceData(**quotas), - status=Status.ACTIVE, - gid=g.gid_number, - folders=GroupFolders( - tier1_work=f"{BASE_PATH_TIER1}/work/groups/{name}", - tier1_scratch=f"{BASE_PATH_TIER1}/scratch/groups/{name}", - tier2_mirrored=f"{BASE_PATH_TIER2}/mirrored/groups/{name}", - tier2_unmirrored=f"{BASE_PATH_TIER2}/unmirrored/groups/{name}", - ), - ) - - def build_hpcproject(p: LdapGroup, quotas: Dict[str, str]) -> Optional[HpcProjectV2]: - name = strip_prefix(p.cn, POSIX_PROJECT_PREFIX) - if not p.owner_dn: - console_err.log(f"no owner DN for {p.cn}, skipping") - return - members = [] - for uid in p.member_uids: - uid = uid.strip() - user = user_by_uid[uid] - members.append(user.uid) - gid_number = user_by_dn[p.owner_dn].gid_number - if not gid_number: - group = None - else: - group = group_by_gid_number[gid_number].cn - return HpcProjectV2( - name=name, - description=g.description, - group=group, - delegate=user_by_dn[p.delegate_dns[0]].uid if p.delegate_dns else None, - resources_requested=ResourceData(**quotas), - status=Status.ACTIVE, - gid=p.gid_number, - folders=GroupFolders( - tier1_work=f"{BASE_PATH_TIER1}/work/projects/{name}", - tier1_scratch=f"{BASE_PATH_TIER1}/scratch/projects/{name}", - tier2_mirrored=f"{BASE_PATH_TIER2}/mirrored/projects/{name}", - tier2_unmirrored=f"{BASE_PATH_TIER2}/unmirrored/projects/{name}", - ), - members=members, - ) - - # construct the resulting state - hpc_users = [] - hpc_groups = [] - hpc_projects = [] - - for u in system_state.ldap_users.values(): - hpc_user = build_hpcuser(u, user_quotas.get(u.uid, {})) - hpc_users.append(hpc_user) - - for g in system_state.ldap_groups.values(): - if g.cn.startswith(POSIX_AG_PREFIX): - hpc_group = build_hpcgroup( - g, group_quotas.get(strip_prefix(g.cn, prefix=POSIX_AG_PREFIX), {}) - ) - if hpc_group: - hpc_groups.append(hpc_group) - elif g.cn.startswith(POSIX_PROJECT_PREFIX): - hpc_project = build_hpcproject( - g, group_quotas.get(strip_prefix(g.cn, prefix=POSIX_PROJECT_PREFIX), {}) - ) - if hpc_project: - hpc_projects.append(hpc_project) - - return HpcaccessStateV2( - hpc_users=hpc_users, - hpc_groups=hpc_groups, - hpc_projects=hpc_projects, - ) - - class TargetStateComparison: """Helper class that compares two system states. @@ -797,9 +643,7 @@ class TargetStateComparison: to them is disabled. """ - def __init__(self, settings: HpcaccessSettings, src: SystemState, dst: SystemState): - #: Configuration of ``hpc-access`` system to use. - self.settings = settings + def __init__(self, src: SystemState, dst: SystemState): #: Source state self.src = src #: Target state