Skip to content

Commit

Permalink
update lib
Browse files Browse the repository at this point in the history
  • Loading branch information
stavros-k committed Jul 25, 2024
1 parent 41aa212 commit 7fe28dd
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 36 deletions.
2 changes: 1 addition & 1 deletion ix-dev/community/distribution/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords:
- registry
- container
lib_version: 1.0.0
lib_version_hash: 9c07d26150ab40c0f19ae3991ee02b338aecf50e6c3619414d114276d08e1c1f
lib_version_hash: 04058cefffe4eeadb07035ab157987492a31d5708705d6b7153d262beb75a796
maintainers:
- email: [email protected]
name: truenas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ def transform_memory(memory):

result = math.ceil(result)
result = min(result, TOTAL_MEM)
# Convert to Megabytes
result = result / 1024 / 1024
return f"{int(result)}M"
return int(result)
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@
from .cpu import transform_cpu, CPU_COUNT


def migrate_resources(resources):
# Handle empty resources, with sane defaults
if not resources:
return {
"limits": {
"cpus": CPU_COUNT / 2,
"memory": f"{TOTAL_MEM / 1024 / 1024}M",
}
}
def migrate_resources(resources, gpus=None):
gpus = gpus or {}

return {
result = {
"limits": {
"cpus": transform_cpu(resources.get("limits", {}).get("cpu", "")),
"memory": transform_memory(resources.get("limits", {}).get("memory", "")),
"cpus": (CPU_COUNT or 2) / 2,
"memory": {TOTAL_MEM / 1024 / 1024},
}
}

if resources.get("limits", {}).get("cpu", ""):
result["limits"].update(
{"cpus": transform_cpu(resources.get("limits", {}).get("cpu", ""))}
)
if resources.get("limits", {}).get("memory", ""):
result["limits"].update(
{"memory": transform_memory(resources.get("limits", {}).get("memory", ""))}
)

for gpu in gpus.items() if gpus else []:
if gpu[1] > 0 and ("amd" in gpu[0] or "intel" in gpu[0]):
result.update({"gpus": {"use_all_gpus": True}})
break
# We cannot migrate NVIDIA GPUs, as we don't know the UUIDs at this point
# and schema validation will fail.

return result
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
def migrate_storage_item(storage_item):
def migrate_storage_item(storage_item, include_read_only=False):
if not storage_item:
raise ValueError("Expected [storage_item] to be set")

Expand All @@ -16,7 +16,8 @@ def migrate_storage_item(storage_item):
if mount_path:
result.update({"mount_path": mount_path})

result.update({"read_only": storage_item.get("readOnly", False)})
if include_read_only:
result.update({"read_only": storage_item.get("readOnly", False)})
return result


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ services:
"REGISTRY_AUTH_HTPASSWD_PATH": htpasswd_path,
}) %}
{% endif %}
environment: {{ ix_lib.base.environment.envs(app=app_env, user=values.distribution.additional_envs) | tojson }}
environment: {{ ix_lib.base.environment.envs(app=app_env, user=values.distribution.additional_envs, values=values) | tojson }}
{% if not values.network.host_network %}
ports:
- {{ ix_lib.base.ports.get_port(port={"target": values.network.api_port, "published": values.network.api_port}) | tojson }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,90 @@
from . import utils
from .resources import get_nvidia_gpus_reservations


def envs(app: dict | None = None, user: list | None = None):
def envs(app: dict | None = None, user: list | None = None, values: dict | None = None):
app = app or {}
user = user or []
track_env = {**app}
result = {**app}

if not user:
user = []
elif isinstance(user, list):
pass
elif isinstance(user, dict):
user = [{"name": k, "value": v} for k, v in user.items()]
else:
values = values or {}
result = {}

if not values:
utils.throw_error("Values cannot be empty in environment.py")

if not isinstance(user, list):
utils.throw_error(
f"Unsupported type for user environment variables [{type(user)}]"
)

for k in app.keys():
if not k:
# Always set TZ
result.update({"TZ": values.get("TZ", "Etc/UTC")})

# Update envs with nvidia variables
if values.get("resources", {}).get("gpus", {}):
result.update(get_nvidia_env(values.get("resources", {}).get("gpus", {})))

# Update envs with run_as variables
if values.get("run_as"):
result.update(get_run_as_envs(values.get("run_as", {})))

# Make sure we don't manually set any of the above
for item in app.items():
if not item[0]:
utils.throw_error("Environment variable name cannot be empty.")
if item[0] in result:
utils.throw_error(
f"Environment variable [{item[0]}] is already defined automatically from the library."
)
result[item[0]] = item[1]

for item in user:
if not item.get("name"):
utils.throw_error("Environment variable name cannot be empty.")
if item.get("name") in track_env:
if item.get("name") in result:
utils.throw_error(
f"Environment variable [{item['name']}] is already defined from the application developer."
)
track_env[item["name"]] = item.get("value")
result[item["name"]] = item.get("value")

return result


# Sets some common variables that most applications use
def get_run_as_envs(run_as: dict) -> dict:
result = {}
user = run_as.get("user")
group = run_as.get("group")
if user:
result.update(
{
"PUID": user,
"UID": user,
"USER_ID": user,
}
)
if group:
result.update(
{
"PGID": group,
"GID": group,
"GROUP_ID": group,
}
)
return result


def get_nvidia_env(gpus: dict) -> dict:
reservations = get_nvidia_gpus_reservations(gpus)
if not reservations.get("device_ids"):
return {
"NVIDIA_VISIBLE_DEVICES": "void",
}

return {
"NVIDIA_VISIBLE_DEVICES": (
",".join(reservations["device_ids"])
if reservations.get("device_ids")
else "void"
),
"NVIDIA_DRIVER_CAPABILITIES": "all",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from . import utils


def get_header(app_name: str):
return f"""# Welcome to TrueNAS SCALE
Thank you for installing {app_name}!
"""


def get_footer(app_name: str):
return f"""## Documentation
Documentation for {app_name} can be found at https://www.truenas.com/docs.
## Bug reports
If you find a bug in this app, please file an issue at
https://ixsystems.atlassian.net or https://github.com/truenas/apps
## Feature requests or improvements
If you find a feature request for this app, please file an issue at
https://ixsystems.atlassian.net or https://github.com/truenas/apps
"""


def get_notes(app_name: str, body: str = ""):
if not app_name:
utils.throw_error("Expected [app_name] to be set")

return f"{get_header(app_name)}\n\n{body}\n\n{get_footer(app_name)}"


def get_portals(portals: list):
valid_schemes = ["http", "https"]
result = []
for portal in portals:
# Most apps have a single portal, lets default to a standard name
name = portal.get("name", "Web UI")
scheme = portal.get("scheme", "http")
path = portal.get("path", "/")

if not name:
utils.throw_error("Expected [portal.name] to be set")
if name in [p["name"] for p in result]:
utils.throw_error(
f"Expected [portal.name] to be unique, got [{', '.join([p['name'] for p in result]+[name])}]"
)
if scheme not in valid_schemes:
utils.throw_error(
f"Expected [portal.scheme] to be one of [{', '.join(valid_schemes)}], got [{portal['scheme']}]"
)
if not portal.get("port"):
utils.throw_error("Expected [portal.port] to be set")
if not path.startswith("/"):
utils.throw_error(
f"Expected [portal.path] to start with /, got [{portal['path']}]"
)

result.append(
{
"name": name,
"scheme": scheme,
# TODO: Default to something else?
"host": portal.get("host", "0.0.0.0"),
"port": portal["port"],
"path": path,
}
)

return result
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,68 @@
from . import utils


def resources(data):
cpus = str(data.get("limits", {}).get("cpus", 2.0))
memory = str(data.get("limits", {}).get("memory", 4096))
def resources(resources):
gpus = resources.get("gpus", {})
cpus = str(resources.get("limits", {}).get("cpus", 2.0))
memory = str(resources.get("limits", {}).get("memory", 4096))
if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", cpus):
utils.throw_error(f"Expected cpus to be a number or a float, got [{cpus}]")
if not re.match(r"^[1-9][0-9]*$", memory):
raise ValueError(f"Expected memory to be a number, got [{memory}]")

return {
result = {
"limits": {"cpus": cpus, "memory": f"{memory}M"},
"reservations": {"devices": []},
}

if gpus:
gpu_result = get_nvidia_gpus_reservations(gpus)
if gpu_result:
# Appending to devices, as we can later extend this to support other types of devices. Eg. TPUs.
result["reservations"]["devices"].append(get_nvidia_gpus_reservations(gpus))

# Docker does not like empty "things" all around.
if not result["reservations"]["devices"]:
del result["reservations"]

return result


def get_nvidia_gpus_reservations(gpus: dict) -> dict:
"""
Input:
{
"nvidia_gpu_selection": {
"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
"pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
},
}
"""
if not gpus:
return {}

device_ids = []
for gpu in gpus.get("nvidia_gpu_selection", {}).values():
if gpu["use_gpu"]:
device_ids.append(gpu["uuid"])

if not device_ids:
return {}

return {
"capabilities": ["gpu"],
"driver": "nvidia",
"device_ids": device_ids,
}


# Returns the top level devices list
# Accepting other_devices to allow manually adding devices
# directly to the list. (Eg sound devices)
def get_devices(resources: dict, other_devices: list = []) -> list:
gpus = resources.get("gpus", {})
devices = other_devices or []
if gpus.get("use_all_gpus", False):
devices.append("/dev/dri")

return devices

0 comments on commit 7fe28dd

Please sign in to comment.