Skip to content

Commit e98b417

Browse files
committed
Add functionality to export slurm config bucket as controller NFS
1 parent 16bbabc commit e98b417

2 files changed

Lines changed: 104 additions & 105 deletions

File tree

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/modules/slurm_files/scripts/setup_network_storage.py

Lines changed: 73 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import stat
2323
import time
2424
import logging
25+
import uuid
2526

2627
import shutil
2728
from pathlib import Path
@@ -35,41 +36,56 @@
3536

3637
log = logging.getLogger()
3738

38-
def mounts_by_local(mounts):
39+
def mounts_by_local(mounts: list[NSMount]) -> dict[str, NSMount]:
3940
"""convert list of mounts to dict of mounts, local_mount as key"""
40-
return {str(Path(m.local_mount).resolve()): m for m in mounts}
41+
return {str(m.local_mount.resolve()): m for m in mounts}
4142

4243

43-
def _get_default_mounts(lkp: util.Lookup) -> List[NSDict]:
44+
def _get_default_mounts(lkp: util.Lookup) -> list[NSMount]:
4445
if lkp.cfg.disable_default_mounts:
4546
return []
4647
return [
47-
NSDict(
48-
server_ip= "$controller",
49-
remote_mount= path,
50-
local_mount= path,
51-
fs_type= "nfs",
52-
mount_options= "defaults,hard,intr",
48+
NSMount(
49+
server_ip=lkp.controller_mount_server_ip(),
50+
remote_mount=path,
51+
local_mount=path,
52+
fs_type="nfs",
53+
mount_options="defaults,hard,intr",
5354
)
5455
for path in (
5556
dirs.home,
5657
dirs.apps,
5758
)
5859
]
5960

60-
def resolve_network_storage(nodeset=None) -> List[NSMount]:
61+
def get_slurm_bucket_mount() -> NSMount:
62+
bucket, path = util._get_bucket_and_common_prefix()
63+
return NSMount(
64+
fs_type="gcsfuse",
65+
server_ip="",
66+
remote_mount=Path(bucket),
67+
local_mount=dirs.slurm_bucket_mount,
68+
mount_options=f"defaults,_netdev,implicit_dirs,only_dir={path}",
69+
)
70+
71+
def resolve_network_storage() -> List[NSMount]:
6172
"""Combine appropriate network_storage fields to a single list"""
6273
lkp = lookup()
6374

6475
# create dict of mounts, local_mount: mount_info
6576
mounts = mounts_by_local(_get_default_mounts(lkp))
6677

78+
if lkp.is_controller and util.should_mount_slurm_bucket():
79+
mounts.update(mounts_by_local([get_slurm_bucket_mount()]))
80+
6781
# On non-controller instances, entries in network_storage could overwrite
6882
# default exports from the controller. Be careful, of course
69-
mounts.update(mounts_by_local(lkp.cfg.network_storage))
83+
common = [lkp.normalize_ns_mount(m) for m in lkp.cfg.network_storage]
84+
mounts.update(mounts_by_local(common))
7085

7186
if lkp.is_login_node:
72-
login_ns = lkp.cfg.login_groups[util.instance_login_group()].network_storage
87+
login_group = lkp.cfg.login_groups[util.instance_login_group()]
88+
login_ns = [lkp.normalize_ns_mount(m) for m in login_group.network_storage]
7389
mounts.update(mounts_by_local(login_ns))
7490

7591
if lkp.instance_role == "compute":
@@ -78,74 +94,46 @@ def resolve_network_storage(nodeset=None) -> List[NSMount]:
7894
except Exception:
7995
pass # external nodename, skip lookup
8096
else:
81-
mounts.update(mounts_by_local(nodeset.network_storage))
82-
83-
return [lkp.normalize_ns_mount(mnt) for mnt in mounts.values()]
84-
97+
nodeset_ns = [lkp.normalize_ns_mount(m) for m in nodeset.network_storage]
98+
mounts.update(mounts_by_local(nodeset_ns))
8599

86-
def separate_external_internal_mounts(mounts):
87-
"""separate into cluster-external and internal mounts"""
100+
return list(mounts.values())
88101

89-
def internal_mount(mount):
90-
# NOTE: Valid Lustre server_ip can take the form of '<IP>@tcp'
91-
server_ip = mount.server_ip.split("@")[0]
92-
mount_addr = util.host_lookup(server_ip)
93-
return mount_addr == lookup().control_host_addr
94-
95-
return separate(internal_mount, mounts)
96102

103+
def is_controller_mount(mount) -> bool:
104+
# NOTE: Valid Lustre server_ip can take the form of '<IP>@tcp'
105+
server_ip = mount.server_ip.split("@")[0]
106+
mount_addr = util.host_lookup(server_ip)
107+
return mount_addr == lookup().control_host_addr
97108

98109
def setup_network_storage():
99110
"""prepare network fs mounts and add them to fstab"""
100111
log.info("Set up network storage")
101-
# filter mounts into two dicts, cluster-internal and external mounts
102-
112+
103113
all_mounts = resolve_network_storage()
104-
ext_mounts, int_mounts = separate_external_internal_mounts(all_mounts)
105-
106114
if lookup().is_controller:
107-
mounts = ext_mounts
115+
mounts, _ = separate(is_controller_mount, all_mounts)
108116
else:
109-
mounts = ext_mounts + int_mounts
117+
mounts = all_mounts
110118

111119
# Determine fstab entries and write them out
112120
fstab_entries = []
113121
for mount in mounts:
114-
local_mount = Path(mount.local_mount)
115-
remote_mount = mount.remote_mount
122+
local_mount = mount.local_mount
116123
fs_type = mount.fs_type
117124
server_ip = mount.server_ip or ""
125+
src = mount.remote_mount if fs_type == "gcsfuse" else f"{server_ip}:{mount.remote_mount}"
126+
127+
log.info(f"Setting up mount ({fs_type}) {src} to {local_mount}")
118128
util.mkdirp(local_mount)
119129

120-
log.info(
121-
"Setting up mount ({}) {}{} to {}".format(
122-
fs_type,
123-
server_ip + ":" if fs_type != "gcsfuse" else "",
124-
remote_mount,
125-
local_mount,
126-
)
127-
)
128-
129130
mount_options = mount.mount_options.split(",") if mount.mount_options else []
130-
if not mount_options or "_netdev" not in mount_options:
131+
if "_netdev" not in mount_options:
131132
mount_options += ["_netdev"]
132-
133-
if fs_type == "gcsfuse":
134-
fstab_entries.append(
135-
"{0} {1} {2} {3} 0 0".format(
136-
remote_mount, local_mount, fs_type, ",".join(mount_options)
137-
)
138-
)
139-
else:
140-
fstab_entries.append(
141-
"{0}:{1} {2} {3} {4} 0 0".format(
142-
server_ip,
143-
remote_mount,
144-
local_mount,
145-
fs_type,
146-
",".join(mount_options),
147-
)
148-
)
133+
options_line = ",".join(mount_options)
134+
135+
136+
fstab_entries.append(f"{src} {local_mount} {fs_type} {options_line} 0 0")
149137

150138
fstab = Path("/etc/fstab")
151139
if not Path(fstab.with_suffix(".bak")).is_file():
@@ -157,16 +145,16 @@ def setup_network_storage():
157145
f.write(entry)
158146
f.write("\n")
159147

160-
mount_fstab(mounts_by_local(mounts), log)
148+
mount_fstab(mounts, log)
161149
if lookup().cfg.enable_slurm_auth:
162150
slurm_key_mount_handler()
163151
else:
164152
munge_mount_handler()
165153

166154

167-
def mount_fstab(mounts, log):
155+
def mount_fstab(mounts: list[NSMount], log):
168156
"""Wait on each mount, then make sure all fstab is mounted"""
169-
def mount_path(path):
157+
def mount_path(path: Path):
170158
log.info(f"Waiting for '{path}' to be mounted...")
171159
try:
172160
run(f"mount {path}", timeout=120)
@@ -184,8 +172,8 @@ def mount_path(path):
184172
with Executors.thread_pool().with_timeout(MAX_MOUNT_TIMEOUT).with_retry(
185173
retry_policy=retry_policy
186174
) as exe:
187-
for path in mounts:
188-
future = exe.submit(mount_path, path)
175+
for m in mounts:
176+
future = exe.submit(mount_path, m.local_mount)
189177
future_list.append(future)
190178

191179
# Iterate over futures, checking for exceptions
@@ -306,36 +294,34 @@ def setup_nfs_exports():
306294
lkp = util.lookup()
307295
assert lkp.is_controller
308296

309-
310297
# The controller only needs to set up exports for cluster-internal mounts
311-
# switch the key to remote mount path since that is what needs exporting
312-
mounts = resolve_network_storage()
313-
314-
if lkp.cfg.enable_slurm_auth:
315-
mounts.append(lkp.slurm_key_mount)
316-
else:
317-
mounts.append(lkp.munge_mount)
318-
319-
# controller mounts
320-
_, con_mounts = separate_external_internal_mounts(mounts)
321-
con_mounts = {m.remote_mount: m for m in con_mounts}
322-
for nodeset in lkp.cfg.nodeset.values():
323-
# get internal mounts for each nodeset by calling
324-
# resolve_network_storage as from a node in each nodeset
325-
ns_mounts = resolve_network_storage(nodeset=nodeset)
326-
_, int_mounts = separate_external_internal_mounts(ns_mounts)
327-
con_mounts.update({m.remote_mount: m for m in int_mounts})
298+
exported_mounts = [m for m in resolve_network_storage() if is_controller_mount(m)]
299+
300+
# key by remote mount path since that is what needs exporting
301+
to_export = {m.remote_mount: "*(rw,no_subtree_check,no_root_squash)" for m in exported_mounts}
302+
303+
key_mount = lkp.slurm_key_mount if lkp.cfg.enable_slurm_auth else lkp.munge_mount
304+
if is_controller_mount(key_mount):
305+
# Export key mount as read-only
306+
to_export[key_mount.remote_mount] = "*(ro,no_subtree_check,no_root_squash)"
307+
308+
if util.should_mount_slurm_bucket():
309+
mnt = get_slurm_bucket_mount()
310+
# FSID is required for virtual filesystem that is not based on a device
311+
# Also export it as read-only
312+
fsid=str(uuid.uuid4())
313+
to_export[mnt.local_mount] = f"*(ro,no_subtree_check,no_root_squash,fsid={fsid})"
328314

329315
# export path if corresponding selector boolean is True
330-
exports = []
331-
for path in con_mounts:
316+
lines = []
317+
for path,options in to_export.items():
332318
util.mkdirp(Path(path))
333319
run(rf"sed -i '\#{path}#d' /etc/exports", timeout=30)
334-
exports.append(f"{path} *(rw,no_subtree_check,no_root_squash)")
320+
lines.append(f"{path} {options}")
335321

336322
exportsd = Path("/etc/exports.d")
337323
util.mkdirp(exportsd)
338324
with (exportsd / "slurm.exports").open("w") as f:
339325
f.write("\n")
340-
f.write("\n".join(exports))
326+
f.write("\n".join(lines))
341327
run("exportfs -a", timeout=30)

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/modules/slurm_files/scripts/util.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
from importlib import metadata
18-
from typing import Iterable, List, Tuple, Optional, Any, Dict, Sequence, Type, Callable, Literal
17+
from typing import Iterable, List, Tuple, Optional, Any, Dict, Sequence, Type, Callable, Union
1918
import argparse
2019
import base64
2120
from dataclasses import dataclass, field
@@ -34,7 +33,6 @@
3433
import socket
3534
import subprocess
3635
import sys
37-
import tempfile
3836
from enum import Enum
3937
from collections import defaultdict
4038
from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -58,8 +56,7 @@
5856

5957
import google.api_core.exceptions as gExceptions
6058

61-
from requests import get as get_url
62-
from requests.exceptions import RequestException
59+
import requests as requests_lib
6360

6461
import yaml
6562
from addict import Dict as NSDict # type: ignore
@@ -93,6 +90,7 @@ def mkdirp(path: Path) -> None:
9390
munge = Path("/etc/munge"),
9491
secdisk = Path("/mnt/disks/sec"),
9592
log = Path("/var/log/slurm"),
93+
slurm_bucket_mount = Path("/slurm/bucket"),
9694
)
9795

9896
slurmdirs = NSDict(
@@ -428,6 +426,13 @@ def map_with_futures(func, seq):
428426
res = e
429427
yield res
430428

429+
def should_mount_slurm_bucket() -> bool:
430+
try:
431+
return instance_metadata("attributes/slurm_bucket_mount").lower() == "true"
432+
except MetadataNotFoundError:
433+
return False
434+
435+
431436
def _get_bucket_and_common_prefix() -> Tuple[str, str]:
432437
uri = instance_metadata("attributes/slurm_bucket_path")
433438
return parse_bucket_uri(uri)
@@ -1066,18 +1071,20 @@ def backoff_delay(start, timeout=None, ratio=None, count: int = 0):
10661071

10671072
ROOT_URL = "http://metadata.google.internal/computeMetadata/v1"
10681073

1074+
class MetadataNotFoundError(Exception):
1075+
pass
10691076

10701077
def get_metadata(path, root=ROOT_URL):
10711078
"""Get metadata relative to metadata/computeMetadata/v1"""
10721079
HEADERS = {"Metadata-Flavor": "Google"}
10731080
url = f"{root}/{path}"
10741081
try:
1075-
resp = get_url(url, headers=HEADERS)
1082+
resp = requests_lib.get(url, headers=HEADERS)
10761083
resp.raise_for_status()
10771084
return resp.text
1078-
except RequestException:
1079-
log.debug(f"metadata not found ({url})")
1080-
raise Exception(f"failed to get_metadata from {url}")
1085+
except requests_lib.exceptions.HTTPError:
1086+
log.exception(f"metadata not found ({url})")
1087+
raise MetadataNotFoundError(f"failed to get_metadata from {url}")
10811088

10821089

10831090
@lru_cache(maxsize=None)
@@ -1925,10 +1932,16 @@ def job(self, job_id: int) -> Optional[Job]:
19251932
def etc_dir(self) -> Path:
19261933
return Path(self.cfg.output_dir or slurmdirs.etc)
19271934

1928-
def normalize_ns_mount(self, ns: Dict[str, str]) -> NSMount:
1935+
def controller_mount_server_ip(self) -> str:
1936+
return self.control_addr or self.control_host
1937+
1938+
def normalize_ns_mount(self, ns: Union[dict, NSMount]) -> NSMount:
1939+
if isinstance(ns, NSMount):
1940+
return ns
1941+
19291942
server_ip = ns.get("server_ip") or "$controller"
19301943
if server_ip == "$controller":
1931-
server_ip = self.control_addr or self.control_host
1944+
server_ip = self.controller_mount_server_ip()
19321945

19331946
return NSMount(
19341947
server_ip=server_ip,
@@ -1943,30 +1956,30 @@ def munge_mount(self) -> NSMount:
19431956
if self.cfg.munge_mount:
19441957
mnt = self.cfg.munge_mount
19451958
mnt.local_mount = mnt.local_mount or "/mnt/munge"
1959+
return self.normalize_ns_mount(mnt)
19461960
else:
1947-
mnt = NSDict(
1948-
server_ip="$controller",
1949-
local_mount="/mnt/munge",
1961+
return NSMount(
1962+
server_ip=self.controller_mount_server_ip(),
1963+
local_mount=Path("/mnt/munge"),
19501964
remote_mount=dirs.munge,
19511965
fs_type="nfs",
19521966
mount_options="defaults,hard,intr,_netdev",
19531967
)
1954-
return self.normalize_ns_mount(mnt)
19551968

19561969
@property
19571970
def slurm_key_mount(self) -> NSMount:
19581971
if self.cfg.slurm_key_mount:
19591972
mnt = self.cfg.slurm_key_mount
19601973
mnt.local_mount = mnt.local_mount or slurmdirs.key_distribution
1974+
return self.normalize_ns_mount(mnt)
19611975
else:
1962-
mnt = NSDict(
1963-
server_ip="$controller",
1976+
return NSMount(
1977+
server_ip=self.controller_mount_server_ip(),
19641978
local_mount=slurmdirs.key_distribution,
19651979
remote_mount=slurmdirs.key_distribution,
19661980
fs_type="nfs",
19671981
mount_options="defaults,hard,intr,_netdev",
19681982
)
1969-
return self.normalize_ns_mount(mnt)
19701983

19711984
def is_flex_node(self, node: str) -> bool:
19721985
try:

0 commit comments

Comments
 (0)