diff --git a/appliances/nodered/3fc354db-94e0-4ced-adcb-03b1b84c89d9.yaml b/appliances/nodered/3fc354db-94e0-4ced-adcb-03b1b84c89d9.yaml new file mode 100644 index 00000000..b2d858df --- /dev/null +++ b/appliances/nodered/3fc354db-94e0-4ced-adcb-03b1b84c89d9.yaml @@ -0,0 +1,65 @@ +--- +name: Node-Red +version: 1.0.0-1 +one-apps_version: 7.0.0-0 +publisher: Pablo del Arco +publisher_email: pdelarco@opennebula.io +description: |- + Node-RED is a low-code, flow-based programming tool for wiring together devices, APIs, and services. This appliance provides Node-Red + running in a Docker container on Ubuntu 22.04 LTS with VNC access and + SSH key authentication. + + **Node-Red features:** + - Visual flow editor + - Integrations with devices/APIs + - Real-time data transformation + - Dashboard & UI building + - Extensible via nodes + **This appliance provides:** + - Ubuntu 22.04 LTS base operating system + - Docker Engine CE pre-installed and configured + - Node-Red container (nodered/node-red:latest) ready to run + - VNC access for desktop environment + - SSH key authentication from OpenNebula context - Web interface on port 1880 + - Configurable container parameters (ports, volumes, environment variables) + + **Access Methods:** + - VNC: Direct access to desktop environment + - SSH: Key-based authentication from OpenNebula - Web: Node-Red interface at http://VM_IP:1880 + +short_description: Node-Red with VNC access and SSH key auth +tags: +- nodered +- docker +- ubuntu +- container +- vnc +- ssh-key +format: qcow2 +creation_time: 1758888057 +os-id: Ubuntu +os-release: '22.04' +os-arch: x86_64 +hypervisor: KVM +opennebula_version: 7.0 +opennebula_template: + context: + network: 'YES' + ssh_public_key: $USER[SSH_PUBLIC_KEY] + set_hostname: $USER[SET_HOSTNAME] + cpu: '2' + disk: + image: $FILE[IMAGE_ID] + image_uname: $USER[IMAGE_UNAME] + graphics: + listen: 0.0.0.0 + type: vnc + memory: '2048' + name: Node-Red + user_inputs: + oneapp_container_name: 'M|text|Container name|nodered-app|nodered-app' + oneapp_container_ports: 'M|text|Container ports (format: host:container)|1880:1880|1880:1880' + oneapp_container_env: 'O|text|Environment variables (format: VAR1=value1,VAR2=value2)||' + oneapp_container_volumes: 'O|text|Volume mounts (format: /host/path:/container/path)|/data:/data|' + inputs_order: ONEAPP_CONTAINER_NAME,ONEAPP_CONTAINER_PORTS,ONEAPP_CONTAINER_ENV,ONEAPP_CONTAINER_VOLUMES +logo: logos/nodered.png diff --git a/appliances/nodered/README.md b/appliances/nodered/README.md new file mode 100644 index 00000000..ce8cf4b2 --- /dev/null +++ b/appliances/nodered/README.md @@ -0,0 +1,78 @@ +# Node-Red Appliance + +Node-RED is a low-code, flow-based programming tool for wiring together devices, APIs, and services. This appliance provides Node-Red running in a Docker container on Ubuntu 22.04 LTS with VNC access and SSH key authentication. + +## Key Features + +**Node-Red capabilities:** + - Visual flow editor + - Integrations with devices/APIs + - Real-time data transformation + - Dashboard & UI building + - Extensible via nodes +**This appliance provides:** +- Ubuntu 22.04 LTS base operating system +- Docker Engine CE pre-installed and configured +- Node-Red container (nodered/node-red:latest) ready to run +- VNC access for desktop environment +- SSH key authentication from OpenNebula context +- Configurable container parameters (ports, volumes, environment variables) - Web interface on port 1880 + +## Quick Start + +1. **Deploy the appliance** from OpenNebula marketplace +2. **Configure container settings** during VM instantiation: + - Container name: nodered-app + - Port mappings: 1880:1880 + - Environment variables: + - Volume mounts: /data:/data +3. **Access the VM**: + - VNC: Direct desktop access + - SSH: `ssh root@VM_IP` (using OpenNebula context keys) - Web: Node-Red interface at http://VM_IP:1880 + +## Container Configuration + +### Port Mappings +Format: `host_port:container_port,host_port2:container_port2` +Default: `1880:1880` + +### Environment Variables +Format: `VAR1=value1,VAR2=value2` +Default: `` + +### Volume Mounts +Format: `/host/path:/container/path,/host/path2:/container/path2` +Default: `/data:/data` + +## Management Commands + +```bash +# View running containers +docker ps + +# View container logs +docker logs nodered-app + +# Access container shell +docker exec -it nodered-app /bin/bash + +# Restart container +systemctl restart nodered-container.service + +# View container service status +systemctl status nodered-container.service +``` + +## Technical Details + +- **Base OS**: Ubuntu 22.04 LTS +- **Container Runtime**: Docker Engine CE +- **Container Image**: nodered/node-red:latest +- **Default Ports**: 1880:1880 +- **Default Volumes**: /data:/data +- **Memory Requirements**: 2GB minimum +- **Disk Requirements**: 8GB minimum + +## Version History + +See [CHANGELOG.md](CHANGELOG.md) for detailed version history. diff --git a/appliances/nodered/appliance.sh b/appliances/nodered/appliance.sh new file mode 100755 index 00000000..f7e32315 --- /dev/null +++ b/appliances/nodered/appliance.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash + +# Node-Red Appliance Installation Script +# Auto-generated by OpenNebula Docker Appliance Generator +# Docker Image: nodered/node-red:latest + +set -o errexit -o pipefail + +# List of contextualization parameters +ONE_SERVICE_PARAMS=( + 'ONEAPP_CONTAINER_NAME' 'configure' 'Docker container name' 'O|text' + 'ONEAPP_CONTAINER_PORTS' 'configure' 'Docker container port mappings' 'O|text' + 'ONEAPP_CONTAINER_ENV' 'configure' 'Docker container environment variables' 'O|text' + 'ONEAPP_CONTAINER_VOLUMES' 'configure' 'Docker container volume mappings' 'O|text' +) + +# Configuration from user input +DOCKER_IMAGE="nodered/node-red:latest" +DEFAULT_CONTAINER_NAME="nodered-app" +DEFAULT_PORTS="1880:1880" +DEFAULT_ENV_VARS="" +DEFAULT_VOLUMES="/data:/data" +APP_NAME="Node-Red" +APPLIANCE_NAME="nodered" + +### Appliance metadata ############################################### + +# Appliance metadata +ONE_SERVICE_NAME='Node-Red' +ONE_SERVICE_VERSION='1.0' # Appliance version +ONE_SERVICE_BUILD=$(date +%s) +ONE_SERVICE_SHORT_DESCRIPTION='Node-Red Docker Container Appliance' +ONE_SERVICE_DESCRIPTION='Node-Red running in Docker container' +ONE_SERVICE_RECONFIGURABLE=true + +### Appliance functions ############################################## + +service_cleanup() +{ + : +} + +service_install() +{ + export DEBIAN_FRONTEND=noninteractive + +# Update system +apt-get update +apt-get upgrade -y + +# Install Docker +apt-get install -y ca-certificates curl +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +chmod a+r /etc/apt/keyrings/docker.asc + +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + +apt-get update +apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# Enable and start Docker +systemctl enable docker +systemctl start docker + +# Pre-create the data directory +mkdir -p /data +chown 1000:1000 /data + +# Pull Node-RED image during installation +msg info "Pulling Node-RED Docker image" +docker pull $DOCKER_IMAGE + +# Verify the image was pulled +msg info "Verifying Node-RED image was pulled:" +docker images nodered/node-red + +# Configure console auto-login +systemctl stop unattended-upgrades 2>/dev/null || true +systemctl disable unattended-upgrades 2>/dev/null || true +apt-get install -y mingetty +mkdir -p /etc/systemd/system/getty@tty1.service.d +cat > /etc/systemd/system/getty@tty1.service.d/override.conf << 'EOF' +[Service] +ExecStart= +ExecStart=-/sbin/agetty --noissue --autologin root %I $TERM +Type=idle +EOF + +# Configure serial console and set root password +mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d +cat > /etc/systemd/system/serial-getty@ttyS0.service.d/override.conf << 'EOF' +[Service] +ExecStart= +ExecStart=-/sbin/agetty --noissue --autologin root %I 115200,38400,9600 vt102 +Type=idle +EOF +echo 'root:opennebula' | chpasswd +systemctl enable getty@tty1.service serial-getty@ttyS0.service + +# Create welcome message +cat > /etc/profile.d/99-nodered-welcome.sh << 'EOF' +#!/bin/bash +case $- in *i*) ;; *) return;; esac +echo "==================================================" +echo " Node-RED Appliance - Container: nodered-app" +echo " Commands: docker ps | docker logs nodered-app" +echo "==================================================" +EOF +chmod +x /etc/profile.d/99-nodered-welcome.sh + +# Clean up +apt-get autoremove -y +apt-get autoclean +find /var/log -type f -exec truncate -s 0 {} \; + +sync +} + +service_configure() +{ + msg info "Starting Node-RED service configuration" + + # Verify Docker is running + if ! systemctl is-active --quiet docker; then + msg error "Docker service is not running" + return 1 + fi + + msg info "✓ Docker service is running" + return 0 +} + +service_bootstrap() +{ + msg info "Starting Node-RED service bootstrap" + + # Setup and start the Node-RED container + setup_nodered_container + + return $? +} + +# Setup Node-RED container +setup_nodered_container() +{ + local container_name="${ONEAPP_CONTAINER_NAME:-$DEFAULT_CONTAINER_NAME}" + local container_ports="${ONEAPP_CONTAINER_PORTS:-$DEFAULT_PORTS}" + local container_env="${ONEAPP_CONTAINER_ENV:-$DEFAULT_ENV_VARS}" + local container_volumes="${ONEAPP_CONTAINER_VOLUMES:-$DEFAULT_VOLUMES}" + + msg info "Setting up Node-RED container: $container_name" + + # Stop and remove existing container if it exists + if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then + msg info "Stopping existing container: $container_name" + docker stop "$container_name" 2>/dev/null || true + docker rm "$container_name" 2>/dev/null || true + fi + + # Parse port mappings + local port_args="" + if [ -n "$container_ports" ]; then + IFS=',' read -ra PORT_ARRAY <<< "$container_ports" + for port in "${PORT_ARRAY[@]}"; do + port_args="$port_args -p $port" + done + fi + + # Parse environment variables + local env_args="" + if [ -n "$container_env" ]; then + IFS=',' read -ra ENV_ARRAY <<< "$container_env" + for env in "${ENV_ARRAY[@]}"; do + env_args="$env_args -e $env" + done + fi + + # Parse volume mounts + local volume_args="" + if [ -n "$container_volumes" ]; then + IFS=',' read -ra VOL_ARRAY <<< "$container_volumes" + for vol in "${VOL_ARRAY[@]}"; do + local host_path=$(echo "$vol" | cut -d':' -f1) + mkdir -p "$host_path" + volume_args="$volume_args -v $vol" + done + fi + + # Start the container + msg info "Starting Node-RED container with:" + msg info " Ports: $container_ports" + msg info " Environment: ${container_env:-none}" + msg info " Volumes: $container_volumes" + + docker run -d \ + --name "$container_name" \ + --restart unless-stopped \ + $port_args \ + $env_args \ + $volume_args \ + "$DOCKER_IMAGE" 2>&1 | while read line; do msg info " $line"; done + + if [ $? -eq 0 ]; then + msg info "✓ Node-RED container started successfully" + + # Wait for container to be healthy + local max_attempts=30 + local attempt=0 + while [ $attempt -lt $max_attempts ]; do + if docker ps --filter "name=$container_name" --format "{{.Status}}" | grep -q "Up"; then + msg info "✓ Node-RED container is running" + local status=$(docker ps --filter "name=$container_name" --format "{{.Status}}") + msg info " Status: $status" + return 0 + fi + attempt=$((attempt + 1)) + sleep 2 + done + + # Check if container stopped unexpectedly + if docker ps -a --filter "name=$container_name" --format "{{.Status}}" | grep -q "Exited"; then + msg error "✗ Node-RED container stopped unexpectedly" + msg info "Container logs:" + docker logs "$container_name" 2>&1 | tail -10 | while read line; do + msg info " $line" + done + return 1 + fi + else + msg error "✗ Failed to start Node-RED container" + return 1 + fi +} diff --git a/appliances/nodered/context.yaml b/appliances/nodered/context.yaml new file mode 100644 index 00000000..0b71c31e --- /dev/null +++ b/appliances/nodered/context.yaml @@ -0,0 +1,9 @@ +--- +:tests: + 'nodered': + :image_name: nodered.qcow2 + :type: linux + :microenvs: ['context-kvm'] + :slow: true + :enable_netcfg_common: True + :enable_netcfg_ip_methods: True diff --git a/appliances/nodered/context/context.sh b/appliances/nodered/context/context.sh new file mode 100644 index 00000000..0d76931f --- /dev/null +++ b/appliances/nodered/context/context.sh @@ -0,0 +1,7 @@ +ETH0_METHOD='dhcp' +NETWORK='YES' +SET_HOSTNAME='NodeRED' +PASSWORD='opennebula' +ETH0_MAC='00:11:22:33:44:55' +NETCFG_TYPE='nm' +START_SCRIPT_BASE64="Z2F3ayAtaSBpbnBsYWNlIC1mLSAvZXRjL3NzaC9zc2hkX2NvbmZpZyA8PCdFT0YnCkJFR0lOIHsgdXBkYXRlID0gIlBhc3N3b3JkQXV0aGVudGljYXRpb24geWVzIiB9Ci9eWyNcc10qUGFzc3dvcmRBdXRoZW50aWNhdGlvblxzLyB7ICQwID0gdXBkYXRlOyBmb3VuZCA9IDEgfQp7IHByaW50IH0KRU5ERklMRSB7IGlmICghZm91bmQpIHByaW50IHVwZGF0ZSB9CkVPRgoKZ2F3ayAtaSBpbnBsYWNlIC1mLSAvZXRjL3NzaC9zc2hkX2NvbmZpZyA8PCdFT0YnCkJFR0lOIHsgdXBkYXRlID0gIlBlcm1pdFJvb3RMb2dpbiB5ZXMiIH0KL15bI1xzXSpQZXJtaXRSb290TG9naW5ccy8geyAkMCA9IHVwZGF0ZTsgZm91bmQgPSAxIH0KeyBwcmludCB9CkVOREZJTEUgeyBpZiAoIWZvdW5kKSBwcmludCB1cGRhdGUgfQpFT0YKCnN5c3RlbWN0bCByZWxvYWQgc3NoZAoKZWNobyAibmFtZXNlcnZlciAxLjEuMS4xIiA+IC9ldGMvcmVzb2x2LmNvbmYK" diff --git a/appliances/nodered/metadata.yaml b/appliances/nodered/metadata.yaml new file mode 100644 index 00000000..f93421cc --- /dev/null +++ b/appliances/nodered/metadata.yaml @@ -0,0 +1,60 @@ +--- +:app: + :name: nodered + :type: service + :os: + :type: linux + :base: ubuntu2204 + :hypervisor: KVM + :opennebula_version: + - '7.0' + :opennebula_template: + context: + - SSH_PUBLIC_KEY="$USER[SSH_PUBLIC_KEY]" + - SET_HOSTNAME="$USER[SET_HOSTNAME]" + cpu: '2' + memory: '2048' + disk_size: '8192' + graphics: + listen: 0.0.0.0 + type: vnc + inputs_order: 'CONTAINER_NAME,CONTAINER_PORTS,CONTAINER_ENV,CONTAINER_VOLUMES' + logo: logos/nodered.png + user_inputs: + CONTAINER_NAME: 'M|text|Container name|nodered-app|nodered-app' + CONTAINER_PORTS: 'M|text|Container ports (format: host:container)|1880:1880|1880:1880' + CONTAINER_ENV: 'O|text|Environment variables (format: VAR1=value1,VAR2=value2)||' + CONTAINER_VOLUMES: 'O|text|Volume mounts (format: /host/path:/container/path)|/data:/data|' + :context: + :prefixed: true + :params: + :CONTAINER_NAME: 'nodered-app' + :CONTAINER_PORTS: '1880:1880' + :CONTAINER_ENV: '' + :CONTAINER_VOLUMES: '/data:/data' + +:one: + :template: + NAME: base + TEMPLATE: + ARCH: x86_64 + CONTEXT: + NETWORK: 'YES' + SSH_PUBLIC_KEY: "$USER[SSH_PUBLIC_KEY]" + CPU: '2' + CPU_MODEL: + MODEL: host-passthrough + GRAPHICS: + LISTEN: 0.0.0.0 + TYPE: vnc + MEMORY: '2048' + NIC: + NETWORK: vnet + NIC_DEFAULT: + MODEL: virtio + :datastore_name: default + :timeout: '90' + +:infra: + :disk_format: qcow2 + :apps_path: /var/tmp diff --git a/appliances/nodered/tests.yaml b/appliances/nodered/tests.yaml new file mode 100644 index 00000000..c5ac5ba3 --- /dev/null +++ b/appliances/nodered/tests.yaml @@ -0,0 +1,2 @@ +--- +- '00-nodered_basic.rb' diff --git a/appliances/nodered/tests/00-nodered_basic.rb b/appliances/nodered/tests/00-nodered_basic.rb new file mode 100644 index 00000000..6dc097df --- /dev/null +++ b/appliances/nodered/tests/00-nodered_basic.rb @@ -0,0 +1,127 @@ +require_relative '../../../lib/community/app_handler' + +describe 'Node-RED Appliance' do + include_context('vm_handler') + + before(:all) do + # Wait for contextualization to complete (Docker installation and container startup) + puts "Waiting for contextualization to complete..." + sleep 60 + + # Wait for Docker service to be ready + max_wait = 120 + wait_interval = 5 + elapsed = 0 + + loop do + cmd = 'systemctl is-active docker 2>/dev/null' + result = @info[:vm].ssh(cmd) + + if result.success? + puts "Docker service is active" + break + end + + if elapsed >= max_wait + puts "Warning: Docker service not active after #{max_wait} seconds" + break + end + + puts "Waiting for Docker service... (#{elapsed}s/#{max_wait}s)" + sleep wait_interval + elapsed += wait_interval + end + + # Additional wait for container to start + sleep 10 + end + + # Test Docker installation + it 'docker is installed and running' do + cmd = 'which docker' + @info[:vm].ssh(cmd).expect_success + + cmd = 'systemctl is-active docker' + @info[:vm].ssh(cmd).expect_success + end + + # Test Node-RED container image is available + it 'node-red container image is available' do + cmd = 'docker images --format "{{.Repository}}:{{.Tag}}" | grep "nodered/node-red:latest"' + @info[:vm].ssh(cmd).expect_success + end + + # Test data directory exists + it 'data directory exists' do + cmd = 'test -d /data' + @info[:vm].ssh(cmd).expect_success + end + + # Test Node-RED container is running + it 'node-red container is running' do + cmd = 'docker ps --format "{{.Names}}" | grep "nodered-app"' + @info[:vm].ssh(cmd).expect_success + end + + # Test container is responsive + it 'node-red container is responsive' do + cmd = 'docker exec nodered-app echo "Container is running"' + execution = @info[:vm].ssh(cmd) + expect(execution.success?).to be(true) + expect(execution.stdout.strip).to eq('Container is running') + end + + # Test container has correct restart policy + it 'container has restart policy configured' do + cmd = 'docker inspect nodered-app --format "{{.HostConfig.RestartPolicy.Name}}"' + execution = @info[:vm].ssh(cmd) + expect(execution.success?).to be(true) + expect(execution.stdout.strip).to eq('unless-stopped') + end + + # Test container port mapping + it 'container has port 1880 exposed' do + cmd = 'docker port nodered-app 1880' + @info[:vm].ssh(cmd).expect_success + end + + # Test container volume mapping + it 'container has data volume mounted' do + cmd = "docker inspect nodered-app --format '{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' | grep '/data:/data'" + @info[:vm].ssh(cmd).expect_success + end + + # Check if the service framework reports ready + it 'checks oneapps motd' do + cmd = 'cat /etc/motd' + timeout_seconds = 60 + retry_interval_seconds = 5 + + begin + Timeout.timeout(timeout_seconds) do + loop do + execution = @info[:vm].ssh(cmd) + + if execution.exitstatus == 0 && execution.stdout.include?('All set and ready to serve') + expect(execution.exitstatus).to eq(0) + expect(execution.stdout).to include('All set and ready to serve') + break + else + sleep(retry_interval_seconds) + end + end + end + rescue Timeout::Error + fail "Timeout after #{timeout_seconds} seconds: MOTD did not contain 'All set and ready to serve'" + end + end + + # Cleanup: Stop container after tests + after(:all) do + if @info && @info[:vm] + @info[:vm].ssh('docker stop nodered-app || true') + @info[:vm].ssh('docker rm nodered-app || true') + end + end +end + diff --git a/apps-code/community-apps/Makefile.config b/apps-code/community-apps/Makefile.config index 6995d7df..199dd389 100644 --- a/apps-code/community-apps/Makefile.config +++ b/apps-code/community-apps/Makefile.config @@ -7,7 +7,7 @@ VERBOSE := 1 PACKER_LOG := 0 PACKER_HEADLESS := true -SERVICES := lithops lithops_worker rabbitmq ueransim example phoenixrtos srsran openfgs +SERVICES := lithops lithops_worker rabbitmq ueransim example phoenixrtos srsran openfgs nodered .DEFAULT_GOAL := help diff --git a/apps-code/community-apps/packer/nodered/81-configure-ssh.sh b/apps-code/community-apps/packer/nodered/81-configure-ssh.sh new file mode 100644 index 00000000..001ffd6b --- /dev/null +++ b/apps-code/community-apps/packer/nodered/81-configure-ssh.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Configures critical settings for OpenSSH server. + +exec 1>&2 +set -eux -o pipefail + +gawk -i inplace -f- /etc/ssh/sshd_config <<'AWKEOF' +BEGIN { update = "PasswordAuthentication no" } +/^[#\s]*PasswordAuthentication\s/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +AWKEOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'AWKEOF' +BEGIN { update = "PermitRootLogin without-password" } +/^[#\s]*PermitRootLogin\s/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +AWKEOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'AWKEOF' +BEGIN { update = "UseDNS no" } +/^[#\s]*UseDNS\s/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +AWKEOF + +sync diff --git a/apps-code/community-apps/packer/nodered/82-configure-context.sh b/apps-code/community-apps/packer/nodered/82-configure-context.sh new file mode 100644 index 00000000..2278ea94 --- /dev/null +++ b/apps-code/community-apps/packer/nodered/82-configure-context.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Configure and enable service context. + +exec 1>&2 +set -eux -o pipefail + +mv /etc/one-appliance/net-90-service-appliance /etc/one-context.d/ +mv /etc/one-appliance/net-99-report-ready /etc/one-context.d/ + +chown root:root /etc/one-context.d/* +chmod u=rwx,go=rx /etc/one-context.d/* + +sync diff --git a/apps-code/community-apps/packer/nodered/common.pkr.hcl b/apps-code/community-apps/packer/nodered/common.pkr.hcl new file mode 120000 index 00000000..3b9ee73c --- /dev/null +++ b/apps-code/community-apps/packer/nodered/common.pkr.hcl @@ -0,0 +1 @@ +../../../one-apps/packer/common.pkr.hcl \ No newline at end of file diff --git a/apps-code/community-apps/packer/nodered/gen_context b/apps-code/community-apps/packer/nodered/gen_context new file mode 100755 index 00000000..7051bfb7 --- /dev/null +++ b/apps-code/community-apps/packer/nodered/gen_context @@ -0,0 +1,33 @@ +#!/bin/bash +set -eux -o pipefail + +SCRIPT=$(cat <<'MAINEND' +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "PasswordAuthentication yes" } +/^[#\s]*PasswordAuthentication\s/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "PermitRootLogin yes" } +/^[#\s]*PermitRootLogin\s/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +systemctl reload sshd + +echo "nameserver 1.1.1.1" > /etc/resolv.conf +MAINEND +) + +cat< ${var.input_dir}/context/context.sh", + "mkisofs -o ${var.input_dir}/${var.appliance_name}-context.iso -V CONTEXT -J -R ${var.input_dir}/context", + ] + } +} + +# Build VM image +source "qemu" "nodered" { + cpus = 2 + memory = 2048 + accelerator = "kvm" + + iso_url = "../one-apps/export/ubuntu2204.qcow2" + iso_checksum = "none" + + headless = var.headless + + disk_image = true + disk_cache = "unsafe" + disk_interface = "virtio" + net_device = "virtio-net" + format = "qcow2" + disk_compression = false + disk_size = "25000" + + output_directory = var.output_dir + + qemuargs = [ + ["-cpu", "host"], + ["-cdrom", "${var.input_dir}/${var.appliance_name}-context.iso"], + ["-serial", "stdio"], + # MAC addr needs to match ETH0_MAC from context iso + ["-netdev", "user,id=net0,hostfwd=tcp::{{ .SSHHostPort }}-:22"], + ["-device", "virtio-net-pci,netdev=net0,mac=00:11:22:33:44:55"] + ] + + ssh_username = "root" + ssh_password = "opennebula" + ssh_timeout = "900s" + shutdown_command = "poweroff" + vm_name = var.appliance_name +} + +build { + sources = ["source.qemu.nodered"] + + # revert insecure ssh options done by context start_script + provisioner "shell" { + scripts = ["${var.input_dir}/81-configure-ssh.sh"] + } + + provisioner "shell" { + inline_shebang = "/bin/bash -e" + inline = [ + "install -o 0 -g 0 -m u=rwx,g=rx,o= -d /etc/one-appliance/{,service.d/,lib/}", + "install -o 0 -g 0 -m u=rwx,g=rx,o=rx -d /opt/one-appliance/{,bin/}", + ] + } + + provisioner "file" { + sources = [ + "../one-apps/appliances/scripts/net-90-service-appliance", + "../one-apps/appliances/scripts/net-99-report-ready", + ] + destination = "/etc/one-appliance/" + } + provisioner "file" { + sources = [ + "../../lib/common.sh", + "../../lib/functions.sh", + ] + destination = "/etc/one-appliance/lib/" + } + provisioner "file" { + source = "../one-apps/appliances/service.sh" + destination = "/etc/one-appliance/service" + } + provisioner "file" { + sources = ["../../appliances/nodered/appliance.sh"] + destination = "/etc/one-appliance/service.d/" + } + + provisioner "shell" { + scripts = ["${var.input_dir}/82-configure-context.sh"] + } + + provisioner "shell" { + inline_shebang = "/bin/bash -e" + inline = ["/etc/one-appliance/service install && sync"] + } + + post-processor "shell-local" { + execute_command = ["bash", "-c", "{{.Vars}} {{.Script}}"] + environment_vars = [ + "OUTPUT_DIR=${var.output_dir}", + "APPLIANCE_NAME=${var.appliance_name}", + ] + scripts = ["../one-apps/packer/postprocess.sh"] + } +} diff --git a/apps-code/community-apps/packer/nodered/postprocess.sh b/apps-code/community-apps/packer/nodered/postprocess.sh new file mode 100755 index 00000000..c6c9b61e --- /dev/null +++ b/apps-code/community-apps/packer/nodered/postprocess.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -ex + +# Custom postprocess script for Node-RED appliance +# This version preserves the root password instead of disabling it + +timeout 5m virt-sysprep \ + --add ${OUTPUT_DIR}/${APPLIANCE_NAME}.qcow2 \ + --selinux-relabel \ + --hostname localhost.localdomain \ + --run-command 'truncate -s0 -c /etc/machine-id' \ + --delete /etc/resolv.conf + +# Note: Removed --root-password disabled to preserve the password set via context + +# virt-sparsify hang badly sometimes, when this happends +# kill + start again +timeout -s9 5m virt-sparsify --in-place ${OUTPUT_DIR}/${APPLIANCE_NAME}.qcow2 + +# Move the final image to the export directory with proper naming +mkdir -p ../one-apps/export/ +cp ${OUTPUT_DIR}/${APPLIANCE_NAME}.qcow2 ../one-apps/export/${APPLIANCE_NAME}.qcow2 +echo "✅ Final image created: ../one-apps/export/${APPLIANCE_NAME}.qcow2" diff --git a/apps-code/community-apps/packer/nodered/variables.pkr.hcl b/apps-code/community-apps/packer/nodered/variables.pkr.hcl new file mode 100644 index 00000000..525227fa --- /dev/null +++ b/apps-code/community-apps/packer/nodered/variables.pkr.hcl @@ -0,0 +1,20 @@ +variable "appliance_name" { + type = string +} + +variable "version" { + type = string +} + +variable "input_dir" { + type = string +} + +variable "output_dir" { + type = string +} + +variable "headless" { + type = bool + default = true +} diff --git a/lib/community/app_handler.rb b/lib/community/app_handler.rb index 0700122f..cd434540 100644 --- a/lib/community/app_handler.rb +++ b/lib/community/app_handler.rb @@ -34,7 +34,7 @@ prefixed = config[:app][:context][:prefixed] - options = "--context #{app_context(APP_CONTEXT_PARAMS, prefixed)} --disk #{APP_IMAGE_NAME}" + options = "--context \"#{app_context(APP_CONTEXT_PARAMS, prefixed)}\" --disk #{APP_IMAGE_NAME}" # Create a new VM by issuing onetemplate instantiate VM_TEMPLATE @info[:vm] = VM.instantiate(VM_TEMPLATE, true, options) @@ -56,12 +56,12 @@ # @return [String] Comma separated list of context parameters ready to be used with --context on CLI template instantiation # def app_context(app_context_params, prefixed = true) - params = [%(SSH_PUBLIC_KEY=\\"\\$USER[SSH_PUBLIC_KEY]\\"), 'NETWORK="YES"'] + params = [%(SSH_PUBLIC_KEY=\\"\\$USER[SSH_PUBLIC_KEY]\\"), %(NETWORK=\\"YES\\")] prefixed == true ? prefix = 'ONEAPP_' : prefix = '' app_context_params.each do |key, value| - params << "#{prefix}#{key}=\"#{value}\"" + params << %(#{prefix}#{key}=\\"#{value}\\") end return params.join(',') diff --git a/logos/nodered.png b/logos/nodered.png new file mode 100644 index 00000000..898c242b Binary files /dev/null and b/logos/nodered.png differ