Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7dceb35
feat: add support for posixgroup feature creation based on repo GID
2Ryan09 May 4, 2026
f98f725
add unit tests
2Ryan09 May 5, 2026
266dc2b
cryoem only
2Ryan09 May 7, 2026
4ca32b8
move grouper run to Python
2Ryan09 May 7, 2026
23bb341
add --grouper-password-file to daemon commands
2Ryan09 May 26, 2026
001ebab
only pass grouper password file into repo registration
2Ryan09 May 29, 2026
2bc8337
query grouper for gid before repo creation
2Ryan09 May 29, 2026
0245dfe
feed grouper-generated gid to new repo ansible workflow
2Ryan09 May 29, 2026
2fdcfe7
update submodule
2Ryan09 May 29, 2026
ff81d5a
update submodule
2Ryan09 May 29, 2026
5f5ad53
print complete grouper_facts
2Ryan09 May 29, 2026
dab3f00
aggressively scan ansible_facts
2Ryan09 May 29, 2026
5066773
done pass fqn to posix group creation
2Ryan09 May 29, 2026
404260c
update submodule
2Ryan09 May 29, 2026
11e46a0
missed repo_group_name
2Ryan09 May 29, 2026
a5f116d
update submodule
2Ryan09 Jun 2, 2026
6943b6d
update tests ot hanel empty runbook values
2Ryan09 Jun 2, 2026
fba5af6
update submodule
2Ryan09 Jun 2, 2026
c92c01e
update submodule
2Ryan09 Jun 2, 2026
3f3dc72
update submodule
2Ryan09 Jun 2, 2026
1eaee68
update submodule
2Ryan09 Jun 2, 2026
f2c12c6
principal -> repo_principal to avoid naming conflict with ansible-rol…
2Ryan09 Jun 2, 2026
dee1054
update submodule
2Ryan09 Jun 2, 2026
a818953
principal -> repo_principal in tests
2Ryan09 Jun 2, 2026
3d8a289
wrong principal corrected
2Ryan09 Jun 2, 2026
659f495
error if gid fetching failed
Jun 4, 2026
7eb69e9
raise if fail to fetch grouper
2Ryan09 Jun 4, 2026
2f8693a
update submodule + cap gql version
Jun 4, 2026
86ce778
update uv.lock
Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ansible-runner/project
2 changes: 1 addition & 1 deletion coact-facility-overage-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export SDF_COACT_URI=coact.slac.stanford.edu:443/graphql-service

while [ 1 ]; do
date
./venv/bin/python3 ./sdf_click.py coact overage --password-file ./etc/.secrets/password --windows 5 --windows 15 --windows 60 --windows 180 --windows 1440 --verbose --influxdb-url=https://influxdb.slac.stanford.edu:443
./venv/bin/python3 ./sdf_click.py coact overage --password-file ./etc/.secrets/password --grouper-password-file ./etc/.secrets/grouper_password --windows 5 --windows 15 --windows 60 --windows 180 --windows 1440 --verbose --influxdb-url=https://influxdb.slac.stanford.edu:443
sleep 300
done
2 changes: 1 addition & 1 deletion coact-reporegistration-daemon.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh

while [ 1 ]; do
SDF_COACT_URI=coact.slac.stanford.edu/graphql-service ./venv/bin/python3 ./sdf_click.py coactd reporegistration --username=sdf-bot --password-file=etc/.secrets/password -vv
SDF_COACT_URI=coact.slac.stanford.edu/graphql-service ./venv/bin/python3 ./sdf_click.py coactd reporegistration --username=sdf-bot --password-file=etc/.secrets/password --grouper-password-file ./etc/.secrets/grouper_password -vv
sleep 1
done
2 changes: 1 addition & 1 deletion coact-userregistration-daemon.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh

while [ 1 ]; do
SDF_COACT_URI=coact.slac.stanford.edu:443/graphql-service ./venv/bin/python3 ./sdf_click.py coactd userregistration --username sdf-bot --password-file ./etc/.secrets/password -vv
SDF_COACT_URI=coact.slac.stanford.edu:443/graphql-service ./venv/bin/python3 ./sdf_click.py coactd userregistration --username sdf-bot --password-file ./etc/.secrets/password --grouper-password-file ./etc/.secrets/grouper_password -vv
sleep 5
done
153 changes: 145 additions & 8 deletions modules/coactd.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ class Registration(GraphQlSubscriber, AnsibleRunner):
}
"""

def __init__(self, username: str, password_file: str, client_name: str, dry_run: bool = False):
def __init__(self, username: str, password_file: str, client_name: str, dry_run: bool = False, grouper_password_file: str = None):
self.logger = logger
self.username = username
self.password_file = password_file
self.client_name = client_name
self.dry_run = dry_run
self.grouper_password_file = grouper_password_file

def run(self):
"""Main entry point - connect and process subscription requests."""
Expand Down Expand Up @@ -531,8 +532,50 @@ def do_new_repo(
if repo_allocation_end_delta is None:
repo_allocation_end_delta = pdl.duration(years=5)

# For CryoEM repos (ct* / ce*), create a POSIX group via Grouper
repo_gid = None
grouper_name = ""
uses_grouper = (
facility.lower() == 'cryoem'
and (repo.lower().startswith('ct') or repo.lower().startswith('ce'))
)
if uses_grouper:
grouper_name = f"sdf-{facility.lower()}-{repo.lower()}"
try:
if not self.grouper_password_file:
raise ValueError("Grouper password file must be provided for CryoEM ct/ce repos")
grouper_kwargs = dict(
grouper_name=grouper_name,
state="present",
grouper_description=f"POSIX group for {facility} {repo} repository access",
grouper_password_file=self.grouper_password_file
)
grouper_runner = self.run_playbook("coact/grouper.yml", **grouper_kwargs)
repo_gid, __dict__ = self.extract_grouper_values(
grouper_runner,
default_group_name=grouper_name
)
self.logger.info(
f"Retrieved grouper values for {facility}:{repo}: gid={repo_gid}"
)
if repo_gid is not None:
self.logger.info(f"Retrieved repo GID for {facility}:{repo}: {repo_gid}")
else:
self.logger.warning(f"No GID found in grouper playbook results for {facility}:{repo}")
raise RuntimeError("Unable to fetch gid from grouper.")
except Exception as e:
self.logger.warning(f"Failed to create grouper POSIX group for {facility}:{repo}: {e}")
raise

# run the facility tasks for this repo
runner = self.run_playbook("coact/add_repo.yaml", facility=facility, repo=repo)
self.run_playbook(
"coact/add_repo.yaml",
facility=facility,
repo=repo,
repo_principal=principal,
gidNumber=repo_gid,
groupName=grouper_name
)

leaders = [principal]
users = [principal]
Expand All @@ -558,18 +601,105 @@ def do_new_repo(
repo_upserted = self.back_channel.execute(REPO_UPSERT_GQL, repo_create_req)
repo_id = repo_upserted['repoUpsert']['Id']

feature_req = {'repo': {'Id': repo_id}}
# Create a parameterized feature upsert mutation
FEATURE_UPSERT_GQL = gql("""
mutation repoUpsert($repo: RepoInput! ) {
repoUpsertFeature(repo: $repo, feature: { name: "slurm", state: true, options: [] }) {
mutation repoUpsert($repo: RepoInput!, $feature: RepoFeatureInput!) {
repoUpsertFeature(repo: $repo, feature: $feature) {
Id
}
}
""")
feature_upserted = self.back_channel.execute(FEATURE_UPSERT_GQL, feature_req)

# Create slurm feature
slurm_feature_req = {
'repo': {'Id': repo_id},
'feature': {'name': 'slurm', 'state': True, 'options': []}
}
self.back_channel.execute(FEATURE_UPSERT_GQL, slurm_feature_req)

# Create posixgroup feature if GID was obtained from grouper
if repo_gid is not None:
posixgroup_options = [json.dumps({
"name": grouper_name,
"gidNumber": int(repo_gid)
})]

posixgroup_feature_req = {
'repo': {'Id': repo_id},
'feature': {
'name': 'posixgroup',
'state': True,
'options': posixgroup_options
}
}

try:
self.back_channel.execute(FEATURE_UPSERT_GQL, posixgroup_feature_req)
self.logger.info(f"Created posixgroup feature for {facility}:{repo} with GID {repo_gid}")
except Exception as e:
self.logger.warning(f"Failed to create posixgroup feature for {facility}:{repo}: {e}")

return True

def extract_grouper_values(self, runner: ansible_runner.runner.Runner, default_group_name: str = ""):
"""Extract gid and group name from grouper playbook events.

Ansible callbacks can emit task results in different shapes depending on
task type (`s3df_grouper`, `debug`, `set_fact`). This method scans all
events and picks the first non-empty gid and group name found.
"""

def _clean(v):
if v is None:
return None
if isinstance(v, str):
v = v.strip()
return v if v else None
return str(v)

gid = None
group_name = _clean(default_group_name)

for event_data in self.playbook_events(runner):
res = event_data.get('res', None)
if not isinstance(res, dict):
continue

facts = res.get('ansible_facts', {})
facts = facts if isinstance(facts, dict) else {}
msg = res.get('msg', None)
msg = msg if isinstance(msg, dict) else {}

candidates_gid = [
res.get('gid', None),
facts.get('gid', None),
msg.get('gid', None),
(res.get('group', {}) if isinstance(res.get('group', {}), dict) else {}).get('idIndex', None),
]
candidates_group_name = [
res.get('group_name', None),
facts.get('group_name', None),
msg.get('group_name', None),
(res.get('group', {}) if isinstance(res.get('group', {}), dict) else {}).get('name', None),
]

for candidate in candidates_gid:
cleaned = _clean(candidate)
if cleaned is not None:
gid = cleaned
break

for candidate in candidates_group_name:
cleaned = _clean(candidate)
if cleaned is not None:
group_name = cleaned
break

if gid is not None and group_name is not None:
break

return gid, group_name or ""

def upsert_repo_compute_allocation(
self,
repo_id: str,
Expand Down Expand Up @@ -906,8 +1036,14 @@ def do_feature(self, repo, facility, dry_run: bool = False) -> bool:
@coactd.command(name='reporegistration')
@common_options
@registration_options
@click.option(
'--grouper-password-file',
default=None,
type=click.Path(exists=True),
help='Path to file containing the Grouper service account password'
)
@click.pass_context
def repo_registration(ctx, verbose, username, password_file, client_name, dry_run):
def repo_registration(ctx, verbose, username, password_file, client_name, dry_run, grouper_password_file):
"""Workflow for repository maintenance.

Handles NewRepo, RepoMembership, RepoRemoveUser, RepoChangeComputeRequirement,
Expand All @@ -920,7 +1056,8 @@ def repo_registration(ctx, verbose, username, password_file, client_name, dry_ru
username=username,
password_file=password_file,
client_name=client_name or 'sdf-bot-RepoRegistration',
dry_run=dry_run
dry_run=dry_run,
grouper_password_file=grouper_password_file
)
handler.run()

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ requires-python = ">=3.12,<3.13"
dependencies = [
"cliff==3.10.1",
"click>=8.0.0",
"gql[all]>=3.4.1",
"gql[all]>=3.4.1,<4",
"ansible-runner==2.3.1",
"Jinja2>=3.1.2",
"pendulum>=3.2.0",
Expand Down
Loading