Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ansible pylint kubernetes prettytable requests passlib fastapi uvicorn
pip install ansible pylint kubernetes prettytable requests passlib fastapi uvicorn sqlalchemy
- name: Get changed Python files (excluding deleted)
id: changed-files
Expand Down
38 changes: 38 additions & 0 deletions build_stream/infra/db/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Database configuration module."""

import os
from typing import Optional


class DatabaseConfig:
"""Database configuration from environment variables."""

def __init__(self):
self.database_url: str = os.getenv("DATABASE_URL", "")
self.pool_size: int = int(os.getenv("DB_POOL_SIZE", "20"))
self.max_overflow: int = int(os.getenv("DB_MAX_OVERFLOW", "10"))
self.pool_recycle: int = int(os.getenv("DB_POOL_RECYCLE", "3600"))
self.echo: bool = os.getenv("DB_ECHO", "false").lower() == "true"

def validate(self) -> None:
"""Validate required configuration."""
if not self.database_url:
raise ValueError("DATABASE_URL environment variable is required")


# Global config instance
db_config = DatabaseConfig()
67 changes: 67 additions & 0 deletions build_stream/infra/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Database session management."""

from contextlib import contextmanager
from typing import Generator

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

from .config import db_config

engine = create_engine(
db_config.database_url,
pool_size=db_config.pool_size,
max_overflow=db_config.max_overflow,
pool_recycle=db_config.pool_recycle,
echo=db_config.echo,
)

SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
)


@contextmanager
def get_db_session() -> Generator[Session, None, None]:
"""
Context manager for database sessions.
Usage:
with get_db_session() as session:
session.add(obj)
session.commit()
"""
session = SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()


def get_db() -> Generator[Session, None, None]:
"""FastAPI dependency for database sessions."""
db = SessionLocal()
try:
yield db
finally:
db.close()
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
"mysqldb_root_password",
"grafana_password",
"provision_password",
"postgresdb_password",
"postgres_password",
"bmc_password",
"switch_snmp3_password",
"docker_password"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
"pattern": "^[a-zA-Z0-9!@#$%^&*()_+=,.?<>;:{}\\[\\]|-]{6,128}$",
"description": "Password for Dockerhub account. Length must be between 8 and 128 characters and can contain letters, numbers, and special characters."
},
"postgres_user": {
"minLength": 4,
"maxLength": 32,
"pattern": "^(?!root$)[A-Za-z0-9_]{4,32}$",
"description": "Username for Postgres DB. Cannot be 'root'. Allowed characters: letters, digits, and underscore (_). Length must be between 4 and 32 characters."
},
"postgres_password": {
"minLength": 8,
"maxLength": 128,
"pattern": "^[a-zA-Z0-9!@#$%^&*()_+=,.?<>;:{}\\[\\]|-]{6,128}$",
"description": "Password for Postgres DB. Length must be between 8 and 128 characters and can contain letters, numbers, and special characters."
},
"slurm_db_password": {
"minLength": 8,
"maxLength": 32,
Expand Down
9 changes: 9 additions & 0 deletions prepare_oim/prepare_oim.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@
tasks_from: reload_pulp_nginx.yml
when: hostvars['oim']['pulp_protocol_https']

- name: Deploy postgres container
hosts: oim
connection: ssh
gather_facts: false
tags: postgres
roles:
- role: deploy_containers/postgres # noqa:role-name[path]
when: hostvars['localhost']['enable_build_stream'] | default(false) | bool

- name: Deploy build_stream container
hosts: oim
connection: ssh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
nfs_type: "{{ metadata_content.stdout | regex_search('nfs_type:\\s*(\\S+)', '\\1') | first | default('') }}"
pulp_server_ip: "{{ hostvars['localhost']['admin_nic_ip'] }}"
pulp_password: "{{ hostvars['localhost']['pulp_password'] }}"
postgres_user: "{{ hostvars['localhost']['postgres_user'] }}"
postgres_password: "{{ hostvars['localhost']['postgres_password'] }}"
postgres_db_name: "{{ hostvars['localhost']['postgres_db_name'] }}"
no_log: true

- name: Set SELinux option for volume mounts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Environment=PULP_VERIFY_SSL=true
Environment=REQUESTS_CA_BUNDLE=/etc/pulp/certs/pulp_webserver.crt
Environment=SSL_CERT_FILE=/etc/pulp/certs/pulp_webserver.crt

# Database configuration
Environment=DATABASE_URL=postgresql://{{ postgres_user }}:{{ postgres_password }}@localhost:5432/{{ postgres_db_name }}

# Volume mounts (shared from omnia_core)
Volume={{ omnia_path }}/omnia:/opt/omnia{{ selinux_option }}
Volume={{ build_stream_log_dir }}:/var/log/omnia_build_stream{{ selinux_option }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ pulp_webserver_cert: "{{ pulp_certs_dir }}/pulp_webserver.crt"
# Pulp server configuration - will be set dynamically during deployment
pulp_base_url: "https://{{ admin_nic_ip }}:2225"

# PostgreSQL configuration (from postgres role)
postgres_user: "{{ hostvars['localhost']['postgres_user'] }}"
postgres_password: "{{ hostvars['localhost']['postgres_password'] }}"
postgres_db_name: "build_stream"

# Quadlet service file path
build_stream_quadlet_path: "/etc/containers/systemd/{{ build_stream_container_name }}.container"
build_stream_quadlet_file_mode: "0644"
Expand Down
137 changes: 137 additions & 0 deletions prepare_oim/roles/deploy_containers/postgres/tasks/deploy_postgres.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

---

# Check and remove existing postgres container if running
- name: Check if build_stream_postgres service exists
ansible.builtin.systemd:
name: "{{ postgres_container_name }}.service"
register: postgres_service_status
failed_when: false

- name: Stop build_stream_postgres service if running
ansible.builtin.systemd:
name: "{{ postgres_container_name }}.service"
state: stopped
enabled: false
when: postgres_service_status.status is defined
failed_when: false

- name: Check if build_stream_postgres container exists
containers.podman.podman_container_info:
name: "{{ postgres_container_name }}"
register: existing_container_info
failed_when: false

- name: Remove existing build_stream_postgres container
containers.podman.podman_container:
name: "{{ postgres_container_name }}"
state: absent
when: existing_container_info.containers | length > 0

# Get metadata and configuration
- name: Get metadata from omnia_core
containers.podman.podman_container_exec:
name: omnia_core
command: cat {{ oim_metadata_file }}
register: metadata_content
changed_when: false

- name: Extract configuration from metadata
ansible.builtin.set_fact:
omnia_path: "{{ metadata_content.stdout | regex_search('oim_shared_path:\\s*(\\S+)', '\\1') | first }}"
share_option: "{{ metadata_content.stdout | regex_search('omnia_share_option:\\s*(\\S+)', '\\1') | first | default('') }}"
nfs_type: "{{ metadata_content.stdout | regex_search('nfs_type:\\s*(\\S+)', '\\1') | first | default('') }}"

- name: Set SELinux option for volume mounts
ansible.builtin.set_fact:
selinux_option: "{{ ':z' if (share_option != 'NFS' or nfs_type | default('') != 'external') else '' }}"

# Create required directories
- name: Create data directory for build_stream_postgres
ansible.builtin.file:
path: "{{ postgres_data_dir }}"
state: directory
mode: "{{ postgres_dir_mode }}"

- name: Create log directory for build_stream_postgres
ansible.builtin.file:
path: "{{ postgres_log_dir }}"
state: directory
mode: "{{ postgres_dir_mode }}"

# Pull container image
- name: Pull build_stream_postgres image from Docker Hub
containers.podman.podman_image:
name: "{{ postgres_image }}"
tag: "{{ postgres_image_tag }}"
state: present
register: image_pull_result

- name: Display image pull result
ansible.builtin.debug:
msg: "{{ postgres_image_pull_success_msg }}"
verbosity: 2
when: image_pull_result is succeeded

# Deploy container using Quadlet and check deployment status
- name: Deploy postgres container and check deployment status
block:
- name: Create Quadlet service file
ansible.builtin.template:
src: postgres.j2
dest: "{{ postgres_quadlet_path }}"
mode: "{{ postgres_quadlet_file_mode }}"
register: quadlet_out

- name: Reload systemd if Quadlet changed
ansible.builtin.systemd_service:
daemon_reload: true
when: quadlet_out.changed # noqa: no-handler

- name: Enable and start postgres service
ansible.builtin.systemd_service:
name: "{{ postgres_container_name }}.service"
enabled: true
state: started
no_log: true

- name: Restart postgres container if Quadlet changed
ansible.builtin.systemd_service:
state: restarted
name: "{{ postgres_container_name }}.service"
when: quadlet_out.changed # noqa: no-handler
no_log: true

- name: Wait for PostgreSQL to be ready
ansible.builtin.pause:
seconds: "{{ postgres_ready_wait_seconds }}"

- name: Check if postgres container is running after deployment
containers.podman.podman_container_info:
name: "{{ postgres_container_name }}"
register: postgres_container_status

- name: Notify user of postgres container deployment status
ansible.builtin.debug:
msg: "{{ postgres_container_success_msg }}"
when:
- postgres_container_status.containers | length > 0
- postgres_container_status.containers[0].State.Status == 'running'

rescue:
- name: Postgres container deployment failed
ansible.builtin.fail:
msg: "{{ postgres_container_failure_msg }}"
20 changes: 20 additions & 0 deletions prepare_oim/roles/deploy_containers/postgres/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

---

- name: Deploy build_stream_postgres container
ansible.builtin.include_tasks: deploy_postgres.yml
tags:
- postgres
29 changes: 29 additions & 0 deletions prepare_oim/roles/deploy_containers/postgres/templates/postgres.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ===============================================================
# build_stream_postgres Quadlet Service
# PostgreSQL Database for Omnia BuildStream
# ===============================================================
[Unit]
Description=PostgreSQL Database for Omnia BuildStream
After=omnia_core.service
Requires=omnia_core.service

[Container]
ContainerName={{ postgres_container_name }}
HostName={{ postgres_container_name }}
Image={{ postgres_image }}:{{ postgres_image_tag }}
Network=host

# Environment variables
Environment=POSTGRES_USER={{ postgres_user }}
Environment=POSTGRES_PASSWORD={{ postgres_password }}
Environment=POSTGRES_DB={{ postgres_db_name }}

# Volume mounts
Volume={{ postgres_data_dir }}:/var/lib/postgresql/data{{ selinux_option }}
Volume={{ postgres_log_dir }}:/var/log/postgresql{{ selinux_option }}

[Service]
Restart=always

[Install]
WantedBy=multi-user.target default.target
Loading