├── outputs.tf ├── providers.tf ├── locals.tf ├── README-region-agnostic-image.md ├── rg_image_lookup.tf ├── iam.tf ├── schema.yaml ├── compute.tf ├── network.tf ├── variables.tf ├── README.md └── cloudinit.sh /outputs.tf: -------------------------------------------------------------------------------- 1 | output "project_compartment_ocid" { value = local.project_compartment_ocid } 2 | output "vcn_id" { value = oci_core_vcn.vcn.id } 3 | output "public_subnet_id" { value = oci_core_subnet.public.id } 4 | output "dev_vm_public_ip" { value = oci_core_instance.dev.public_ip } -------------------------------------------------------------------------------- /providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | required_providers { 4 | oci = { 5 | source = "oracle/oci" 6 | version = ">= 7.13.0" 7 | } 8 | } 9 | } 10 | 11 | provider "oci" { 12 | region = var.region 13 | } 14 | 15 | provider "oci" { 16 | alias = "home" 17 | region = var.home_region 18 | } -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | parent_for_project = trimspace(var.parent_compartment_ocid) != "" ? var.parent_compartment_ocid : var.tenancy_ocid 3 | project_compartment_ocid = var.create_compartment ? oci_identity_compartment.project[0].id : var.project_compartment_ocid 4 | 5 | # CSV -> numbers 6 | ports_strings = [for p in split(",", var.open_tcp_ports_csv) : trimspace(p)] 7 | open_tcp_ports = [for p in local.ports_strings : tonumber(p)] 8 | } -------------------------------------------------------------------------------- /README-region-agnostic-image.md: -------------------------------------------------------------------------------- 1 | # Region-Agnostic Oracle Linux 8 Image Selection 2 | 3 | This update removes hard-coded per-region image OCIDs and automatically discovers the latest Oracle Linux 8 image 4 | in the **current region** of the stack/provider. It preserves your existing flow, and you can still override the image 5 | via the optional `image_ocid` variable when needed (e.g., for locked exam builds). 6 | 7 | ## How it works 8 | - Adds `rg_image_lookup.tf` that uses `data "oci_core_images"` filtered for Oracle Linux 8. 9 | - Defines `local.rg_effective_image_ocid` which prefers `var.image_ocid` (if provided) otherwise falls back to the newest OL8 image. 10 | - Rewrites `image_id` within `source_details {}` to reference `local.rg_effective_image_ocid`. 11 | 12 | No other flow changes are required. The ORM-selected region is used automatically. 13 | -------------------------------------------------------------------------------- /rg_image_lookup.tf: -------------------------------------------------------------------------------- 1 | variable "image_ocid" { 2 | description = "Optional override: if set, use this image OCID instead of auto-discovery." 3 | type = string 4 | default = "" 5 | } 6 | 7 | # Region-agnostic Oracle Linux 8 image discovery in the CURRENT region of the provider/ORM stack 8 | data "oci_core_images" "rg_ol8_latest" { 9 | compartment_id = var.tenancy_ocid 10 | operating_system = "Oracle Linux" 11 | operating_system_version = "8" 12 | state = "AVAILABLE" 13 | sort_by = "TIMECREATED" 14 | sort_order = "DESC" 15 | 16 | # Use the selected shape to ensure architecture compatibility (x86 vs aarch64) 17 | shape = var.instance_shape 18 | 19 | # Keep only standard OL8 images (excludes minimal/specialized) 20 | filter { 21 | name = "display_name" 22 | values = ["^Oracle-Linux-8.*$"] 23 | regex = true 24 | } 25 | } 26 | 27 | locals { 28 | rg_discovered_image_ocid = length(data.oci_core_images.rg_ol8_latest.images) > 0 ? data.oci_core_images.rg_ol8_latest.images[0].id : "" 29 | rg_effective_image_ocid = trimspace(var.image_ocid) != "" ? var.image_ocid : local.rg_discovered_image_ocid 30 | } 31 | -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | resource "oci_identity_compartment" "project" { 2 | count = var.create_compartment ? 1 : 0 3 | provider = oci.home 4 | compartment_id = local.parent_for_project 5 | description = "GenAI one-click project compartment" 6 | name = var.compartment_name 7 | } 8 | 9 | resource "oci_identity_dynamic_group" "dg" { 10 | count = var.create_policies ? 1 : 0 11 | provider = oci.home 12 | compartment_id = var.tenancy_ocid 13 | description = "GenAI OneClick DG" 14 | name = "oneclick-genai-dg-${substr(replace(local.project_compartment_ocid, "ocid1.compartment.oc1..", ""), 0, 8)}" 15 | matching_rule = "ANY {instance.compartment.id = '${local.project_compartment_ocid}'}" 16 | } 17 | 18 | resource "oci_identity_policy" "dg_policies" { 19 | count = var.create_policies ? 1 : 0 20 | provider = oci.home 21 | compartment_id = var.tenancy_ocid 22 | description = "Allow DG to use Generative AI and related services" 23 | name = "oneclick-genai-dg-policies-${substr(replace(local.project_compartment_ocid, "ocid1.compartment.oc1..", ""), 0, 8)}" 24 | statements = [ 25 | "allow dynamic-group ${oci_identity_dynamic_group.dg[0].name} to use generative-ai-family in tenancy", 26 | "allow dynamic-group ${oci_identity_dynamic_group.dg[0].name} to read compartments in tenancy", 27 | "allow dynamic-group ${oci_identity_dynamic_group.dg[0].name} to manage object-family in compartment id ${local.project_compartment_ocid}" 28 | ] 29 | } -------------------------------------------------------------------------------- /schema.yaml: -------------------------------------------------------------------------------- 1 | title: "OCI One-Click GenAI Stack — v15j" 2 | schemaVersion: 1.1.0 3 | variables: 4 | tenancy_ocid: { title: "Tenancy OCID", type: string, required: true } 5 | home_region: { title: "Home Region", type: string, required: true } 6 | region: { title: "Deployment Region", type: string, required: true } 7 | ssh_public_key: { title: "SSH Public Key", type: string, required: true } 8 | create_compartment: { title: "Create Project Compartment", type: boolean, default: true } 9 | parent_compartment_ocid: { title: "Parent Compartment OCID (for new project compartment)", type: string } 10 | project_compartment_ocid: { title: "Existing Project Compartment OCID (when not creating)", type: string } 11 | compartment_name: { title: "Project Compartment Name", type: string, default: "genai-oneclick-project" } 12 | instance_shape: { title: "Instance Shape", type: string, default: "VM.Standard.E5.Flex" } 13 | instance_ocpus: { title: "OCPUs", type: number, default: 2 } 14 | instance_memory_gbs: { title: "Memory (GB)", type: number, default: 24 } 15 | boot_volume_size_gbs: { title: "Boot Volume Size (GB)", type: number, default: 100 } 16 | open_tcp_ports_csv: { title: "Open TCP Ports (CSV)", type: string, default: "22,8888,8501,1521" } 17 | create_policies: { title: "Create DG + Policies for Generative AI", type: boolean, default: true } 18 | variableGroups: 19 | - title: "Basics" 20 | variables: [tenancy_ocid, home_region, region, ssh_public_key] 21 | - title: "Compartments" 22 | variables: [create_compartment, parent_compartment_ocid, project_compartment_ocid, compartment_name] 23 | - title: "Compute" 24 | variables: [instance_shape, instance_ocpus, instance_memory_gbs, boot_volume_size_gbs, open_tcp_ports_csv] 25 | - title: "IAM" 26 | variables: [create_policies] -------------------------------------------------------------------------------- /compute.tf: -------------------------------------------------------------------------------- 1 | data "oci_identity_availability_domains" "ads" { 2 | compartment_id = var.tenancy_ocid 3 | provider = oci.home 4 | } 5 | 6 | data "oci_core_images" "ol9" { 7 | compartment_id = var.tenancy_ocid 8 | operating_system = "Oracle Linux" 9 | operating_system_version = "8" 10 | shape = var.instance_shape 11 | sort_by = "TIMECREATED" 12 | sort_order = "DESC" 13 | } 14 | 15 | resource "oci_core_instance" "dev" { 16 | availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name 17 | compartment_id = local.project_compartment_ocid 18 | display_name = "GEN-AI-LABS" 19 | 20 | create_vnic_details { 21 | subnet_id = oci_core_subnet.public.id 22 | assign_public_ip = true 23 | hostname_label = "genaivm" 24 | } 25 | 26 | shape = var.instance_shape 27 | 28 | shape_config { 29 | ocpus = var.instance_ocpus 30 | memory_in_gbs = var.instance_memory_gbs 31 | } 32 | 33 | source_details { 34 | source_type = "image" 35 | source_id = local.rg_effective_image_ocid 36 | } 37 | 38 | metadata = { 39 | ssh_authorized_keys = var.ssh_public_key 40 | user_data = filebase64("${path.module}/cloudinit.sh") 41 | } 42 | 43 | agent_config { 44 | is_management_disabled = false 45 | is_monitoring_disabled = false 46 | } 47 | 48 | launch_options { network_type = "PARAVIRTUALIZED" } 49 | instance_options { are_legacy_imds_endpoints_disabled = true } 50 | 51 | timeouts { create = "60m" } 52 | 53 | lifecycle { 54 | precondition { 55 | condition = var.create_compartment || (trimspace(var.project_compartment_ocid) != "") 56 | error_message = "When create_compartment=false you must provide project_compartment_ocid." 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /network.tf: -------------------------------------------------------------------------------- 1 | resource "oci_core_vcn" "vcn" { 2 | compartment_id = local.project_compartment_ocid 3 | display_name = "GENAILABS-VCN" 4 | cidr_blocks = ["10.0.0.0/16"] 5 | dns_label = "genailabs" 6 | } 7 | 8 | resource "oci_core_internet_gateway" "igw" { 9 | compartment_id = local.project_compartment_ocid 10 | vcn_id = oci_core_vcn.vcn.id 11 | enabled = true 12 | display_name = "GENAILABS-IGW" 13 | } 14 | 15 | resource "oci_core_route_table" "rt" { 16 | compartment_id = local.project_compartment_ocid 17 | vcn_id = oci_core_vcn.vcn.id 18 | display_name = "GENAILABS-RT" 19 | 20 | route_rules { 21 | network_entity_id = oci_core_internet_gateway.igw.id 22 | description = "Default route" 23 | destination = "0.0.0.0/0" 24 | destination_type = "CIDR_BLOCK" 25 | } 26 | } 27 | 28 | resource "oci_core_subnet" "public" { 29 | compartment_id = local.project_compartment_ocid 30 | vcn_id = oci_core_vcn.vcn.id 31 | display_name = "GENAILABS-SNET" 32 | cidr_block = "10.0.10.0/24" 33 | prohibit_public_ip_on_vnic = false 34 | route_table_id = oci_core_route_table.rt.id 35 | security_list_ids = [oci_core_security_list.public.id] 36 | dns_label = "genailabs" 37 | } 38 | 39 | resource "oci_core_security_list" "public" { 40 | compartment_id = local.project_compartment_ocid 41 | vcn_id = oci_core_vcn.vcn.id 42 | display_name = "GENAILABS-SL" 43 | 44 | # All egress 45 | egress_security_rules { 46 | protocol = "all" 47 | destination = "0.0.0.0/0" 48 | description = "All egress" 49 | } 50 | 51 | # Ingress for each TCP port 52 | dynamic "ingress_security_rules" { 53 | for_each = local.open_tcp_ports 54 | content { 55 | protocol = "6" 56 | source = "0.0.0.0/0" 57 | description = "Allow TCP port ${ingress_security_rules.value}" 58 | tcp_options { 59 | min = ingress_security_rules.value 60 | max = ingress_security_rules.value 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "tenancy_ocid" { 2 | description = "OCID of your tenancy (root)." 3 | type = string 4 | } 5 | 6 | variable "home_region" { 7 | description = "Your tenancy's home region (e.g., ap-hyderabad-1)." 8 | type = string 9 | } 10 | 11 | variable "region" { 12 | description = "Deployment region (e.g., ap-hyderabad-1)." 13 | type = string 14 | } 15 | 16 | variable "create_compartment" { 17 | description = "Whether to create a new project compartment." 18 | type = bool 19 | default = true 20 | } 21 | 22 | variable "parent_compartment_ocid" { 23 | description = "Parent compartment OCID for new project compartment. Leave blank to use tenancy root." 24 | type = string 25 | default = "" 26 | } 27 | 28 | variable "project_compartment_ocid" { 29 | description = "Existing compartment OCID when create_compartment=false." 30 | type = string 31 | default = "" 32 | } 33 | 34 | variable "compartment_name" { 35 | description = "Project compartment name." 36 | type = string 37 | default = "genai-oneclick-project" 38 | } 39 | 40 | variable "ssh_public_key" { 41 | description = "Your SSH public key (ssh-rsa ...)." 42 | type = string 43 | } 44 | 45 | variable "instance_shape" { 46 | description = "Compute shape." 47 | type = string 48 | default = "VM.Standard.E5.Flex" 49 | } 50 | 51 | variable "instance_ocpus" { 52 | description = "OCPUs for Flex shape." 53 | type = number 54 | default = 2 55 | } 56 | 57 | variable "instance_memory_gbs" { 58 | description = "Memory (GB) for Flex shape." 59 | type = number 60 | default = 24 61 | } 62 | 63 | variable "boot_volume_size_gbs" { 64 | description = "Boot volume size (GB)." 65 | type = number 66 | default = 100 67 | } 68 | 69 | variable "open_tcp_ports_csv" { 70 | description = "TCP ports to open (CSV), e.g. 22,8888,8501,1521" 71 | type = string 72 | default = "22,8888,8501,1521" 73 | } 74 | 75 | variable "create_policies" { 76 | description = "Create instance-principal DG and Policies for Generative AI?" 77 | type = bool 78 | default = true 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GenAI RAG Stack (OCI Resource Manager) 2 | 3 | A one-click **OCI Resource Manager** (ORM) stack that provisions a minimal environment for Gen-AI/RAG experiments: 4 | - VCN/subnet + routing + security (as defined in the stack) 5 | - Compute instance (flex) with cloud-init bootstrap 6 | - IAM/Dynamic Group/Policies (if enabled) 7 | - **Region-agnostic Oracle Linux 8 image auto-discovery** (no hard-coded OCIDs). 8 | You can still override with `image_ocid` if you must pin a specific image. 9 | 10 | > ✅ This version auto-selects the latest Oracle Linux 8 image in the region where you deploy the stack, filtered by `instance_shape` to avoid x86/ARM mismatches. 11 | 12 | --- 13 | 14 | ## Deploy with One Click 15 | 16 | [![Deploy to Oracle Cloud](https://oci-resourcemanager-plugin.plugins.oci.oraclecloud.com/latest/deploy-to-oracle-cloud.svg)](https://cloud.oracle.com/resourcemanager/stacks/create?zipUrl=https://github.com/ou-developers/GenAI-RAG-Sandbox-Stack/archive/refs/heads/main.zip) 17 | 18 | ### Alternative: Upload Manually 19 | 1. Download the ZIP release asset. 20 | 2. In OCI Console → **Developer Services → Resource Manager → Stacks → Create Stack**. 21 | 3. Choose **Upload Zip File** and select the ZIP. 22 | 4. Configure variables → **Plan** → **Apply**. 23 | 24 | --- 25 | 26 | ## Prerequisites 27 | 28 | - OCI tenancy + permission to create Resource Manager stacks and target resources (VCN, instance, policies). 29 | - SSH public key (if the instance expects it) ready to paste. 30 | - Your **Tenancy OCID**, **Home Region**, and **Deployment Region**. 31 | - (Optional) A specific **image OCID** if you need to lock to a given OS image build. 32 | 33 | --- 34 | 35 | ## Inputs (common variables) 36 | 37 | > The exact list is in `variables.tf`; here are the typical ones you’ll see: 38 | 39 | - `tenancy_ocid` *(string, required)* – Tenancy OCID. 40 | - `home_region` *(string, required)* – Home region (e.g., `ap-hyderabad-1`). 41 | - `region` *(string, required)* – Deployment region (e.g., `ap-mumbai-1`). 42 | *If omitted in provider, ORM’s selected region is used.* 43 | - `create_compartment` *(bool, default: true)* – Create a project compartment or use an existing one. 44 | - `project_compartment_ocid` *(string, optional)* – If not creating a compartment, provide one. 45 | - `project_compartment_name` *(string, optional)* – Name when creating a compartment. 46 | - `instance_shape` *(string, required)* – e.g., `VM.Standard.E5.Flex` (the stack sets OCPUs/memory separately). 47 | - `instance_ocpus` *(number, required for flex shapes)*. 48 | - `instance_memory_gbs` *(number, required for flex shapes)*. 49 | - `ssh_public_key` *(string, required)* – Paste your `~/.ssh/id_rsa.pub` (or equivalent). 50 | - `open_tcp_ports_csv` *(string, default provided)* – e.g., `22,8888,8501,1521`. 51 | - `create_policies` *(bool, default: true)* – Create Dynamic Group + Policies for service access. 52 | - `image_ocid` *(string, optional)* – **Override**: use this instead of auto-discovered OL8. 53 | 54 | --- 55 | 56 | ## How Region-Agnostic Image Selection Works 57 | 58 | - The stack uses `data "oci_core_images"` with: 59 | - `operating_system = "Oracle Linux"`, `operating_system_version = "8"` 60 | - `shape = var.instance_shape` (ensures correct arch/virt type) 61 | - `sort_by = TIMECREATED`, `sort_order = DESC` (picks newest) 62 | - It sets: 63 | - `local.rg_effective_image_ocid = var.image_ocid != "" ? var.image_ocid : discovered_ol8_image_id` 64 | - The instance launches from `local.rg_effective_image_ocid`. 65 | 66 | This eliminates per-region OCID maps and “invalid parameter” errors when switching regions. 67 | 68 | --- 69 | 70 | ## Outputs 71 | 72 | Open the **Outputs** tab after Apply. Typical values include: 73 | - Instance OCID / Public IP / Private IP 74 | - Subnet / VCN identifiers 75 | - (If enabled) IAM/DG/Policy names or OCIDs 76 | 77 | --- 78 | 79 | ## Post-Deploy: Verify Setup 80 | 81 | SSH into the instance and run: 82 | 83 | ```bash 84 | # Did cloud-init finish? 85 | sudo cloud-init status --wait && sudo cloud-init status --long 86 | 87 | # Any failed services? 88 | systemctl --failed 89 | 90 | # Example package check (customize to your stack): 91 | rpm -q httpd git unzip python3 python3-pip 92 | ``` 93 | 94 | **Background processes (e.g., Jupyter):** 95 | - quick: `nohup bash start_jupyter.sh > jupyter.out 2>&1 & disown` 96 | - robust: create a user `systemd` service 97 | 98 | --- 99 | 100 | ## Clean Up 101 | 102 | To avoid charges: 103 | 1. In ORM → your stack → **Jobs**, run **Destroy**. 104 | 2. Delete the stack (and any manual leftovers if you created resources outside the stack). 105 | 106 | --- 107 | 108 | ## Changelog 109 | 110 | - **v2 (region-agnostic-fix1)** 111 | - Auto-discover latest **Oracle Linux 8** image in the deploy region 112 | - `instance_shape`-aware image filtering 113 | - Optional `image_ocid` override 114 | - Fixed ternary & trimspace issue in image lookup 115 | 116 | --- 117 | 118 | ## License 119 | 120 | MIT (or your preferred license). Add a `LICENSE` file if needed. 121 | -------------------------------------------------------------------------------- /cloudinit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # cloudinit.sh — Oracle 23ai Free + GenAI stack bootstrap (merged) 3 | # - Keeps ALL prior provisioning (code/, start_jupyter.sh, OCI CLI, pyenv, etc.) 4 | # - Installs Podman first so DB unit can run 5 | # - DB unit runs FIRST; setup unit runs AFTER DB 6 | # - Robust DB bootstrap: open FREEPDB1, SAVE STATE, wait for listener 7 | # - Idempotent creation of PDB user vector/vector 8 | # - Hardcoded ORACLE_PWD=database123 (as requested) 9 | 10 | set -Eeuo pipefail 11 | 12 | LOGFILE="/var/log/genai_setup.log" 13 | exec > >(tee -a "$LOGFILE") 2>&1 14 | echo "===== GenAI OneClick: start $(date -u) =====" 15 | 16 | # -------------------------------------------------------------------- 17 | # Grow filesystem (best-effort) 18 | # -------------------------------------------------------------------- 19 | if command -v /usr/libexec/oci-growfs >/dev/null 2>&1; then 20 | /usr/libexec/oci-growfs -y || true 21 | fi 22 | 23 | # -------------------------------------------------------------------- 24 | # PRE: install Podman so the DB unit can run right away 25 | # -------------------------------------------------------------------- 26 | echo "[PRE] installing Podman and basics" 27 | 28 | # Disable problematic repositories that might cause connectivity issues 29 | dnf config-manager --set-disabled ol8_ksplice || true 30 | 31 | # Clean and refresh cache 32 | dnf clean all || true 33 | dnf makecache --refresh || true 34 | 35 | # Enable required repositories with error handling 36 | dnf config-manager --set-enabled ol8_addons || true 37 | dnf config-manager --set-enabled ol8_appstream || true 38 | dnf config-manager --set-enabled ol8_baseos_latest || true 39 | 40 | # Install core packages with retries 41 | install_with_retry() { 42 | local max_attempts=3 43 | local attempt=1 44 | 45 | while [ $attempt -le $max_attempts ]; do 46 | echo "[PRE] Installation attempt $attempt of $max_attempts" 47 | if dnf -y install podman curl grep coreutils shadow-utils git unzip; then 48 | echo "[PRE] Installation successful" 49 | return 0 50 | else 51 | echo "[PRE] Installation attempt $attempt failed, retrying..." 52 | sleep 10 53 | attempt=$((attempt + 1)) 54 | fi 55 | done 56 | 57 | echo "[PRE] All installation attempts failed" 58 | return 1 59 | } 60 | 61 | # Try installation with retries 62 | if ! install_with_retry; then 63 | echo "[PRE] Critical: Could not install required packages" 64 | exit 1 65 | fi 66 | 67 | # Verify critical tools are available 68 | if ! command -v podman >/dev/null 2>&1; then 69 | echo "[PRE] Critical: podman not found after installation" 70 | exit 1 71 | fi 72 | 73 | /usr/bin/podman --version || { echo "[PRE] podman installation verification failed"; exit 1; } 74 | echo "[PRE] Successfully installed Podman and dependencies" 75 | 76 | # ==================================================================== 77 | # genai-setup.sh (MAIN provisioning) — kept from your original, with small fixes 78 | # ==================================================================== 79 | cat >/usr/local/bin/genai-setup.sh <<'SCRIPT' 80 | #!/bin/bash 81 | set -uxo pipefail 82 | 83 | echo "===== GenAI OneClick systemd: start $(date -u) =====" 84 | 85 | MARKER="/var/lib/genai.oneclick.done" 86 | if [[ -f "$MARKER" ]]; then 87 | echo "[INFO] already provisioned; exiting." 88 | exit 0 89 | fi 90 | 91 | retry() { local max=${1:-5}; shift; local n=1; until "$@"; do rc=$?; [[ $n -ge $max ]] && echo "[RETRY] failed after $n: $*" && return $rc; echo "[RETRY] $n -> retrying in $((n*5))s: $*"; sleep $((n*5)); n=$((n+1)); done; return 0; } 92 | 93 | echo "[STEP] enable ol8_addons, pre-populate metadata, and install base pkgs" 94 | retry 5 dnf -y install dnf-plugins-core curl 95 | retry 5 dnf config-manager --set-enabled ol8_addons || true 96 | retry 5 dnf -y makecache --refresh 97 | retry 5 dnf -y install \ 98 | git unzip jq tar make gcc gcc-c++ bzip2 bzip2-devel zlib-devel openssl-devel readline-devel libffi-devel \ 99 | wget curl which xz python3 python3-pip podman firewalld 100 | 101 | echo "[STEP] enable firewalld" 102 | systemctl enable --now firewalld || true 103 | 104 | echo "[STEP] create /opt/genai and /home/opc/code" 105 | mkdir -p /opt/genai /home/opc/code /home/opc/bin 106 | chown -R opc:opc /opt/genai /home/opc/code /home/opc/bin 107 | 108 | echo "[STEP] create /home/opc/code and fetch css-navigator/gen-ai" 109 | CODE_DIR="/home/opc/code" 110 | mkdir -p "$CODE_DIR" 111 | 112 | # preflight 113 | if ! command -v git >/dev/null 2>&1; then retry 5 dnf -y install git; fi 114 | if ! command -v curl >/dev/null 2>&1; then retry 5 dnf -y install curl; fi 115 | if ! command -v unzip >/dev/null 2>&1; then retry 5 dnf -y install unzip; fi 116 | 117 | TMP_DIR="$(mktemp -d)" 118 | REPO_ZIP="/tmp/cssnav.zip" 119 | 120 | # Try sparse checkout 121 | retry 5 git clone --depth 1 --filter=blob:none --sparse https://github.com/ou-developers/css-navigator.git "$TMP_DIR" || true 122 | retry 5 git -C "$TMP_DIR" sparse-checkout init --cone || true 123 | retry 5 git -C "$TMP_DIR" sparse-checkout set gen-ai || true 124 | 125 | if [ -d "$TMP_DIR/gen-ai" ] && [ -n "$(ls -A "$TMP_DIR/gen-ai" 2>/dev/null)" ]; then 126 | echo "[STEP] copying from sparse-checkout" 127 | chmod -R a+rx "$TMP_DIR/gen-ai" || true 128 | cp -a "$TMP_DIR/gen-ai"/. "$CODE_DIR"/ 129 | else 130 | echo "[STEP] sparse-checkout empty; falling back to zip" 131 | retry 5 curl -L -o "$REPO_ZIP" https://codeload.github.com/ou-developers/css-navigator/zip/refs/heads/main 132 | TMP_ZIP_DIR="$(mktemp -d)" 133 | unzip -q -o "$REPO_ZIP" -d "$TMP_ZIP_DIR" 134 | if [ -d "$TMP_ZIP_DIR/css-navigator-main/gen-ai" ]; then 135 | chmod -R a+rx "$TMP_ZIP_DIR/css-navigator-main/gen-ai" || true 136 | cp -a "$TMP_ZIP_DIR/css-navigator-main/gen-ai"/. "$CODE_DIR"/ 137 | else 138 | echo "[WARN] gen-ai folder not found in zip" 139 | fi 140 | rm -rf "$TMP_ZIP_DIR" "$REPO_ZIP" 141 | fi 142 | 143 | rm -rf "$TMP_DIR" 144 | 145 | # ownership and a backward-compat symlink 146 | chown -R opc:opc "$CODE_DIR" || true 147 | chmod -R a+rX "$CODE_DIR" || true 148 | ln -sfn "$CODE_DIR" /opt/code || true 149 | 150 | echo "[STEP] embed user's init-genailabs.sh (modified to NOT start DB; it waits for it)" 151 | cat >/opt/genai/init-genailabs.sh <<'USERSCRIPT' 152 | #!/bin/bash 153 | set -Eeuo pipefail 154 | LOGFILE=/var/log/cloud-init-output.log 155 | exec > >(tee -a $LOGFILE) 2>&1 156 | 157 | MARKER_FILE="/home/opc/.init_done" 158 | if [ -f "$MARKER_FILE" ]; then 159 | echo "Init script has already been run. Exiting." 160 | exit 0 161 | fi 162 | 163 | echo "===== Starting Cloud-Init User Script =====" 164 | 165 | # Expand the boot volume (best-effort) 166 | sudo /usr/libexec/oci-growfs -y || true 167 | 168 | # Ensure build prerequisites (SQLite-from-source path kept) 169 | sudo dnf config-manager --set-enabled ol8_addons || true 170 | sudo dnf install -y podman git libffi-devel bzip2-devel ncurses-devel readline-devel wget make gcc zlib-devel openssl-devel || true 171 | 172 | # Install latest SQLite from source (kept from original) 173 | cd /tmp 174 | wget -q https://www.sqlite.org/2023/sqlite-autoconf-3430000.tar.gz 175 | tar -xzf sqlite-autoconf-3430000.tar.gz 176 | cd sqlite-autoconf-3430000 177 | ./configure --prefix=/usr/local 178 | make -s 179 | sudo make install 180 | 181 | # Verify SQLite 182 | /usr/local/bin/sqlite3 --version || true 183 | 184 | # PATH/LD for SQLite (kept) 185 | echo 'export PATH="/usr/local/bin:$PATH"' >> /home/opc/.bashrc 186 | echo 'export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"' >> /home/opc/.bashrc 187 | echo 'export CFLAGS="-I/usr/local/include"' >> /home/opc/.bashrc 188 | echo 'export LDFLAGS="-L/usr/local/lib"' >> /home/opc/.bashrc 189 | source /home/opc/.bashrc 190 | 191 | # Persistent oradata 192 | sudo mkdir -p /home/opc/oradata 193 | sudo chown -R 54321:54321 /home/opc/oradata 194 | sudo chmod -R 755 /home/opc/oradata 195 | 196 | # >>> Modified: DO NOT start the DB here. The systemd DB unit owns it. <<< 197 | # Wait for 23ai container to exist, then for FREEPDB1 service 198 | echo "Waiting for 23ai container to be created..." 199 | for i in {1..120}; do 200 | if /usr/bin/podman ps -a --format '{{.Names}}' | grep -qw 23ai; then 201 | echo "23ai container exists." 202 | break 203 | fi 204 | sleep 5 205 | done 206 | 207 | echo "Waiting for FREEPDB1 service to be registered with the listener..." 208 | for i in {1..180}; do 209 | if /usr/bin/podman exec 23ai bash -lc '. /home/oracle/.bashrc; lsnrctl status' | grep -qi 'Service "FREEPDB1"'; then 210 | echo "FREEPDB1 service is registered." 211 | break 212 | fi 213 | sleep 10 214 | done 215 | 216 | # Quick connection smoke (non-fatal) 217 | OUTPUT=$(/usr/bin/podman exec 23ai bash -lc 'echo | sqlplus -S -L sys/database123@127.0.0.1:1521/FREEPDB1 as sysdba || true') 218 | echo "$OUTPUT" | tail -n 2 219 | 220 | # PDB config (kept, but guarded) 221 | echo "Configuring Oracle database in PDB (FREEPDB1)..." 222 | sudo /usr/bin/podman exec -i 23ai bash -lc '. /home/oracle/.bashrc; sqlplus -S -L "sys:database123@127.0.0.1:1521/FREEPDB1 as sysdba" <> $HOME/.bashrc 252 | export PYENV_ROOT="\$HOME/.pyenv" 253 | [[ -d "\$PYENV_ROOT/bin" ]] && export PATH="\$PYENV_ROOT/bin:\$PATH" 254 | eval "\$(pyenv init --path)" 255 | eval "\$(pyenv init -)" 256 | eval "\$(pyenv virtualenv-init -)" 257 | EOF 258 | 259 | cat << EOF >> $HOME/.bash_profile 260 | if [ -f ~/.bashrc ]; then 261 | source ~/.bashrc 262 | fi 263 | EOF 264 | 265 | source $HOME/.bashrc 266 | export PATH="$PYENV_ROOT/bin:$PATH" 267 | 268 | CFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib" LD_LIBRARY_PATH="/usr/local/lib" pyenv install -s 3.11.9 269 | pyenv rehash 270 | mkdir -p $HOME/labs 271 | cd $HOME/labs 272 | pyenv local 3.11.9 273 | pyenv rehash 274 | python --version 275 | export PYTHONPATH=$HOME/.pyenv/versions/3.11.9/lib/python3.11/site-packages:$PYTHONPATH 276 | 277 | $HOME/.pyenv/versions/3.11.9/bin/pip install --no-cache-dir oci==2.129.1 oracledb transformers==4.46.3 sentence-transformers==3.3.1 langchain==0.2.6 langchain-community==0.2.6 langchain-chroma==0.1.2 langchain-core==0.2.11 langchain-text-splitters==0.2.2 langsmith==0.1.83 pypdf==4.2.0 streamlit==1.36.0 python-multipart==0.0.9 chroma-hnswlib==0.7.3 chromadb==0.5.3 torch==2.5.0 278 | python - <> $HOME/.bashrc 288 | source $HOME/.bashrc 289 | 290 | REPO_URL="https://github.com/ou-developers/ou-generativeai-pro.git" 291 | FINAL_DIR="$HOME/labs" 292 | git init 293 | git remote add origin $REPO_URL 294 | git config core.sparseCheckout true 295 | echo "labs/*" >> .git/info/sparse-checkout 296 | git pull origin main || true 297 | mv labs/* . 2>/dev/null || true 298 | rm -rf .git labs 299 | echo "Files successfully downloaded to $FINAL_DIR" 300 | EOF_OPC 301 | 302 | touch "$MARKER_FILE" 303 | echo "===== Cloud-Init User Script Completed Successfully =====" 304 | exit 0 305 | USERSCRIPT 306 | chmod +x /opt/genai/init-genailabs.sh 307 | cp -f /opt/genai/init-genailabs.sh /home/opc/init-genailabs.sh || true 308 | chown opc:opc /home/opc/init-genailabs.sh || true 309 | 310 | echo "[STEP] install Python 3.9 for OL8 and create venv" 311 | retry 5 dnf -y module enable python39 || true 312 | retry 5 dnf -y install python39 python39-pip 313 | sudo -u opc bash -lc 'python3.9 -m venv $HOME/.venvs/genai || true; echo "source $HOME/.venvs/genai/bin/activate" >> $HOME/.bashrc; source $HOME/.venvs/genai/bin/activate; python -m pip install --upgrade pip wheel setuptools' 314 | echo "[STEP] install Python libraries" 315 | sudo -u opc bash -lc 'source $HOME/.venvs/genai/bin/activate; pip install --no-cache-dir jupyterlab==4.2.5 streamlit==1.36.0 oracledb transformers==4.46.3 sentence-transformers==3.3.1 langchain==0.2.6 langchain-community==0.2.6 langchain-core==0.2.11 langchain-text-splitters==0.2.2 langsmith==0.1.83 pypdf==4.2.0 python-multipart==0.0.9 chroma-hnswlib==0.7.3 chromadb==0.5.3 torch==2.5.0 oci oracle-ads' 316 | echo "[STEP] install OCI CLI to ~/bin/oci and make PATH global" 317 | sudo -u opc bash -lc 'retry 5 curl -sSL https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh -o /tmp/oci-install.sh; retry 5 bash /tmp/oci-install.sh --accept-all-defaults --exec-dir $HOME/bin --install-dir $HOME/lib/oci-cli --update-path false; grep -q "export PATH=$HOME/bin" $HOME/.bashrc || echo "export PATH=$HOME/bin:$PATH" >> $HOME/.bashrc' 318 | cat >/etc/profile.d/genai-path.sh <<'PROF' 319 | export PATH=/home/opc/bin:$PATH 320 | PROF 321 | 322 | echo "[STEP] seed /opt/genai content" 323 | cat >/opt/genai/LoadProperties.py <<'PY' 324 | class LoadProperties: 325 | def __init__(self): 326 | import json 327 | with open('config.txt') as f: 328 | js = json.load(f) 329 | self.model_name = js.get("model_name") 330 | self.embedding_model_name = js.get("embedding_model_name") 331 | self.endpoint = js.get("endpoint") 332 | self.compartment_ocid = js.get("compartment_ocid") 333 | def getModelName(self): return self.model_name 334 | def getEmbeddingModelName(self): return self.embedding_model_name 335 | def getEndpoint(self): return self.endpoint 336 | def getCompartment(self): return self.compartment_ocid 337 | PY 338 | cat >/opt/genai/config.txt <<'CFG' 339 | {"model_name":"cohere.command-r-16k","embedding_model_name":"cohere.embed-english-v3.0","endpoint":"https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com","compartment_ocid":"ocid1.compartment.oc1....replace_me..."} 340 | CFG 341 | mkdir -p /opt/genai/txt-docs /opt/genai/pdf-docs 342 | echo "faq | What are Always Free services?=====Always Free services are part of Oracle Cloud Free Tier." >/opt/genai/txt-docs/faq.txt 343 | chown -R opc:opc /opt/genai 344 | 345 | echo "[STEP] write start_jupyter.sh" 346 | cat >/home/opc/start_jupyter.sh <<'SH' 347 | #!/bin/bash 348 | set -eux 349 | source $HOME/.venvs/genai/bin/activate 350 | jupyter lab --NotebookApp.token='' --NotebookApp.password='' --ip=0.0.0.0 --port=8888 --no-browser 351 | SH 352 | chown opc:opc /home/opc/start_jupyter.sh 353 | chmod +x /home/opc/start_jupyter.sh 354 | 355 | echo "[STEP] open firewall ports" 356 | for p in 8888 8501 1521; do firewall-cmd --zone=public --add-port=${p}/tcp --permanent || true; done 357 | firewall-cmd --reload || true 358 | 359 | echo "[STEP] run user's init-genailabs.sh (non-fatal)" 360 | set +e 361 | bash /opt/genai/init-genailabs.sh 362 | USR_RC=$? 363 | set -e 364 | echo "[STEP] user init script exit code: $USR_RC" 365 | 366 | touch "$MARKER" 367 | echo "===== GenAI OneClick systemd: COMPLETE $(date -u) =====" 368 | SCRIPT 369 | chmod +x /usr/local/bin/genai-setup.sh 370 | 371 | # ==================================================================== 372 | # genai-db.sh (DB container) — robust bootstrap for 23ai 373 | # ==================================================================== 374 | cat >/usr/local/bin/genai-db.sh <<'DBSCR' 375 | #!/bin/bash 376 | set -Eeuo pipefail 377 | 378 | PODMAN="/usr/bin/podman" 379 | log(){ echo "[DB] $*"; } 380 | retry() { local t=${1:-5}; shift; local n=1; until "$@"; do local rc=$?; 381 | if (( n>=t )); then return "$rc"; fi 382 | log "retry $n/$t (rc=$rc): $*"; sleep $((n*5)); ((n++)); 383 | done; } 384 | 385 | ORACLE_PWD="database123" 386 | ORACLE_PDB="FREEPDB1" 387 | ORADATA_DIR="/home/opc/oradata" 388 | IMAGE="container-registry.oracle.com/database/free:latest" 389 | NAME="23ai" 390 | 391 | log "start $(date -u)" 392 | mkdir -p "$ORADATA_DIR" && chown -R 54321:54321 "$ORADATA_DIR" || true 393 | 394 | retry 5 "$PODMAN" pull "$IMAGE" || true 395 | "$PODMAN" rm -f "$NAME" || true 396 | 397 | retry 5 "$PODMAN" run -d --name "$NAME" --network=host \ 398 | -e ORACLE_PWD="$ORACLE_PWD" \ 399 | -e ORACLE_PDB="$ORACLE_PDB" \ 400 | -e ORACLE_MEMORY='2048' \ 401 | -v "$ORADATA_DIR":/opt/oracle/oradata:z \ 402 | "$IMAGE" 403 | 404 | log "waiting for 'DATABASE IS READY TO USE!'" 405 | for i in {1..144}; do 406 | "$PODMAN" logs "$NAME" 2>&1 | grep -q 'DATABASE IS READY TO USE!' && break 407 | sleep 5 408 | done 409 | 410 | log "opening PDB and saving state..." 411 | "$PODMAN" exec -e ORACLE_PWD="$ORACLE_PWD" -i "$NAME" bash -lc ' 412 | . /home/oracle/.bashrc 413 | sqlplus -S -L /nolog </etc/systemd/system/genai-23ai.service <<'UNIT_DB' 452 | [Unit] 453 | Description=GenAI oneclick - Oracle 23ai container 454 | Wants=network-online.target 455 | After=network-online.target 456 | 457 | [Service] 458 | Type=oneshot 459 | RemainAfterExit=yes 460 | TimeoutStartSec=0 461 | KillMode=process 462 | ExecStart=/bin/bash -lc '/usr/local/bin/genai-db.sh >> /var/log/genai_setup.log 2>&1' 463 | Restart=no 464 | 465 | [Install] 466 | WantedBy=multi-user.target 467 | UNIT_DB 468 | 469 | cat >/etc/systemd/system/genai-setup.service <<'UNIT_SETUP' 470 | [Unit] 471 | Description=GenAI oneclick post-boot setup 472 | Wants=network-online.target genai-23ai.service 473 | After=network-online.target genai-23ai.service 474 | 475 | [Service] 476 | Type=oneshot 477 | RemainAfterExit=yes 478 | ExecStart=/bin/bash -lc '/usr/local/bin/genai-setup.sh >> /var/log/genai_setup.log 2>&1' 479 | Restart=no 480 | 481 | [Install] 482 | WantedBy=multi-user.target 483 | UNIT_SETUP 484 | 485 | systemctl daemon-reload 486 | systemctl enable genai-23ai.service 487 | systemctl enable genai-setup.service 488 | systemctl start genai-23ai.service # DB/bootstrap first 489 | systemctl start genai-setup.service # then app/setup 490 | 491 | echo "===== GenAI OneClick: cloud-init done $(date -u) =====" 492 | --------------------------------------------------------------------------------