├── .dockerignore ├── src ├── common │ ├── index.ts │ ├── interface │ │ ├── index.ts │ │ └── model.ts │ ├── params │ │ ├── index.ts │ │ ├── hzn-params.ts │ │ ├── api-params.ts │ │ └── params.ts │ └── utils.ts ├── bee-stack.ts ├── find-node.ts └── server.ts ├── image.png ├── image-1.png ├── image-2.png ├── entrypoint.sh ├── watch-container-stop.sh ├── .gitignore ├── service.definition.json ├── setup.sh ├── package.json ├── README.md ├── tsconfig.json ├── Dockerfile-amd64 ├── Dockerfile-arm64 ├── sample-template.json └── bee-stack.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /src/common/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model'; 2 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playground/open-horizon-bee-stack/main/image.png -------------------------------------------------------------------------------- /image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playground/open-horizon-bee-stack/main/image-1.png -------------------------------------------------------------------------------- /image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playground/open-horizon-bee-stack/main/image-2.png -------------------------------------------------------------------------------- /src/common/params/index.ts: -------------------------------------------------------------------------------- 1 | export * from './params'; 2 | export * from './api-params'; 3 | export * from './hzn-params'; -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start the main process (e.g., your app) 4 | echo "Starting main container process..." 5 | node dist/find-node.js 6 | 7 | -------------------------------------------------------------------------------- /src/common/params/hzn-params.ts: -------------------------------------------------------------------------------- 1 | import { ApiParams } from "./api-params"; 2 | 3 | export interface HznParams extends ApiParams { 4 | type: string; 5 | id: string; 6 | object: string; 7 | pattern: string; 8 | url: string; 9 | } -------------------------------------------------------------------------------- /src/common/params/api-params.ts: -------------------------------------------------------------------------------- 1 | import { Params } from "./params"; 2 | 3 | export interface ApiParams extends Params { 4 | days: string; 5 | language: string; 6 | units: string; 7 | basin: string; 8 | triggerFields: any; 9 | trigger_identity: string; 10 | user: any; 11 | apiKey: string; 12 | insightKey: string; 13 | } -------------------------------------------------------------------------------- /src/bee-stack.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './server'; 2 | 3 | const argv: string = process.argv.slice(2).toString() 4 | const match = argv.match(/--port=/) 5 | const port = match ? parseInt(argv.replace(match[0], '')) : 8889; 6 | 7 | export class Index { 8 | server = new Server(port); 9 | constructor() { 10 | 11 | } 12 | } 13 | 14 | new Index() -------------------------------------------------------------------------------- /watch-container-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to handle stop signal (SIGTERM) 4 | on_exit() { 5 | echo "OHBS container stopping... executing stop script." 6 | /app/bee-stack/bee-stack.sh stop 7 | sleep 45 8 | } 9 | # Trap SIGTERM signal and execute `on_exit` 10 | trap on_exit SIGTERM 11 | # Start the main process (e.g., your app) 12 | echo "Starting main container process..." 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /src/common/params/params.ts: -------------------------------------------------------------------------------- 1 | export interface Params { 2 | message: string; 3 | hash: string; 4 | method: string; 5 | func: string; 6 | bucket: string; 7 | directory: any; 8 | delimiter: string; 9 | acl: string; 10 | filename: any; 11 | files: any[]; 12 | apiKeyId: string; 13 | accessKeyId: string; 14 | secretAccessKey: string; 15 | endpoint: string; 16 | ibmAuthEndpoint: string; 17 | serviceInstanceId: string; 18 | region: string; 19 | cacheControl: string; 20 | body: any; 21 | name: string; 22 | contentType: string; 23 | label: string; 24 | key: string; 25 | date: string; 26 | purge: string; 27 | lat: string; 28 | lng: string; 29 | days: string; 30 | language: string; 31 | units: string; 32 | basin: string; 33 | expires: string; 34 | value: any; 35 | sessionId: string; 36 | sig: string; 37 | addr: string; 38 | url: string; 39 | } 40 | -------------------------------------------------------------------------------- /service.definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "org": "$HZN_ORG_ID", 3 | "label": "$SERVICE_NAME for $ARCH", 4 | "url": "$SERVICE_NAME", 5 | "version": "$SERVICE_VERSION", 6 | "arch": "$ARCH", 7 | "public": true, 8 | "sharable": "singleton", 9 | "requiredServices": [], 10 | "userInput": [ 11 | { "name": "LLM_SELECTED_OPT", "label": "", "type": "string", "defaultValue": "$LLM_SELECTED_OPT" }, 12 | { "name": "WATSONX_PROJECT_ID", "label": "", "type": "string", "defaultValue": "$WATSONX_PROJECT_ID" }, 13 | { "name": "WATSONX_API_KEY", "label": "", "type": "string", "defaultValue": "$WATSONX_API_KEY" }, 14 | { "name": "WATSONX_REGION", "label": "", "type": "string", "defaultValue": "$WATSONX_REGION" }, 15 | { "name": "AUTO_YES_NO", "label": "", "type": "string", "defaultValue": "$AUTO_YES_NO" } 16 | ], 17 | "deployment": { 18 | "services": { 19 | "$SERVICE_NAME": { 20 | "image": "$SERVICE_CONTAINER", 21 | "binds": ["$MMS_SHARED_VOLUME:$VOLUME_MOUNT:rw","/var/run/docker.sock:/var/run/docker.sock"], 22 | "ports": [ 23 | ], 24 | "network_mode": "host", 25 | "privileged": true 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Define the repository URL and branch (modify as needed) 7 | REPO_URL="https://github.com/i-am-bee/bee-stack.git" 8 | CLONE_DIR="bee-stack" 9 | 10 | if [ -d "$CLONE_DIR/.git" ]; then 11 | echo "Directory '$CLONE_DIR' exists and is a git repository. Pulling latest changes..." 12 | cd "$CLONE_DIR" 13 | git pull 14 | else 15 | echo "Directory '$CLONE_DIR' does not exist or is not a git repository. Cloning..." 16 | git clone "$REPO_URL" 17 | fi 18 | 19 | # Get the current working directory's basename 20 | current_dir=$(basename "$(pwd)") 21 | 22 | # Check if the current directory is 'app' 23 | if [ "$current_dir" == "app" ]; then 24 | # Navigate into the cloned repository 25 | cd "$CLONE_DIR" || { echo "Failed to cd into $CLONE_DIR"; exit 1; } 26 | fi 27 | 28 | echo "Current directory is: $(pwd)" 29 | 30 | # Ensure setup.sh is executable 31 | if [ -f "./bee-stack.sh" ]; then 32 | chmod +x ./bee-stack.sh 33 | echo "Executing bee-stack.sh..." 34 | ./bee-stack.sh setup 35 | else 36 | echo "setup.sh not found in $CLONE_DIR." 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-horizon-bee-stack", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "rebuild": "del-cli --force dist && npm run build", 8 | "build": "tsc", 9 | "build:arm64-image": "docker build -t playbox21/open-horizon-bee-stack-express_arm64:$npm_package_version --no-cache -f Dockerfile-arm64 .", 10 | "build:amd64-image": "docker build -t playbox21/open-horizon-bee-stack-express_amd64:$npm_package_version --platform linux/amd64 --no-cache -f Dockerfile-amd64 .", 11 | "start": "node dist/find-node.js", 12 | "test": "jest --coverage", 13 | "test:watch": "jest --watch && npm run build:watch" 14 | }, 15 | "keywords": [], 16 | "author": "ljeff@us.ibm.com", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@types/jest": "^29.5.14", 20 | "cors": "^2.8.5", 21 | "express": "^4.21.2", 22 | "express-fileupload": "^1.5.1", 23 | "jest": "^29.7.0", 24 | "jsonfile": "^6.1.0", 25 | "rxjs": "^7.8.1", 26 | "ts-jest": "^29.2.5" 27 | }, 28 | "devDependencies": { 29 | "@types/express": "^5.0.0", 30 | "@types/node": "^22.10.1", 31 | "del-cli": "^6.0.0", 32 | "dotenv": "^16.4.7", 33 | "nodemon": "^3.1.7", 34 | "typescript": "^5.7.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Open Horizon Bee Stack 2 | This project is intended to integrate Open Horizon with Bee Stack to lower barrier of entry and streamline development effort. 3 | 4 | Open Horizon is a platform for managing the service software lifecycle of containerized workloads and related machine learning assets. It enables autonomous management of applications deployed to distributed webscale fleets of edge computing nodes and devices without requiring on-premise administrators. You can fine more info here https://open-horizon.github.io/ 5 | 6 | ### Installation 7 | If you can't use Docker Desktop 8 | https://developer-stage.dc4.usva.ibm.com/blogs/awb-rancher-desktop-alternative-to-docker-desktop. 9 | 10 | Make a copy of sample-template.json and fill in the necessary values 11 | 12 | Run 13 | ``` 14 | curl -sSL https://raw.githubusercontent.com/playground/hzn-cli/main/install.sh --output install.sh && bash ./install.sh 15 | ``` 16 | Select Run-In-Containers 17 | ![alt text](image.png) 18 | 19 | Select Provide configuration file path 20 | ![alt text](image-1.png) 21 | 22 | Enter path to your tempate file, for example "/home/oh/oh-in-container.json" 23 | ![alt text](image-2.png) 24 | 25 | Confirm and proceed to install. Once installation is complete, open-horizon-bee-stack container will be started. 26 | 27 | Go inside the container to start Bee Stack setup -------------------------------------------------------------------------------- /src/find-node.ts: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'), 2 | exec = cp.exec; 3 | 4 | let timer; 5 | const argv: string = process.argv.slice(2).toString() 6 | const match = argv.match(/--port=/) 7 | const port = match ? parseInt(argv.replace(match[0], '')) : 8889; 8 | 9 | const find = (name) => { 10 | exec(`ps -ef | grep "${name}"`, {maxBuffer: 1024 * 2000}, (err, stdout, stderr) => { 11 | if(!err) { 12 | const lines = stdout.split('\n') 13 | let found = false; 14 | lines.forEach((line, i) => { 15 | if(line.length > 0 && !line.match(/-ef|grep/) && !line.match(/grep node dist\/bee-stack.js/)) { 16 | if(line.match(/node dist\/bee-stack.js/)) { 17 | found = true; 18 | } 19 | } 20 | }) 21 | if(!found) { 22 | clearInterval(timer); 23 | console.log(`restarting node server here ${port}`) 24 | const child = exec(`node dist/bee-stack.js --port=${port}`, (err, stdout, stderr) => { 25 | }); 26 | child.stdout.pipe(process.stdout); 27 | child.on('data', (data) => { 28 | console.log(data) 29 | }) 30 | } 31 | sleep(2000).then(() => { 32 | setCheckInterval(8000); 33 | }) 34 | } 35 | }) 36 | } 37 | 38 | const exist = (instance) => { 39 | return instance.cmd === 'node dist/bee-stack.js'; 40 | } 41 | 42 | const setCheckInterval = (ms) => { 43 | clearInterval(timer); 44 | timer = setInterval(() => { 45 | find('node dist/bee-stack.js'); 46 | }, ms); 47 | }; 48 | 49 | const sleep = (ms) => { 50 | return new Promise(resolve => setTimeout(resolve, ms)); 51 | } 52 | 53 | find('node'); 54 | 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "commonjs", /* Specify what module code is generated. */ 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 7 | "resolveJsonModule": true, /* Enable importing .json files */ 8 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 9 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 10 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 11 | "importHelpers": false, 12 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 13 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 14 | "strict": false, /* Enable all strict type-checking options. */ 15 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 16 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 17 | "types": [ 18 | "node", 19 | "jest" 20 | ], 21 | "typeRoots": [ 22 | "@types", 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "include": [ 27 | "./src/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "./src/**/*.test.*", 31 | "node_modules", "dist" 32 | ] 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Dockerfile-amd64: -------------------------------------------------------------------------------- 1 | # Start with a base image that includes git 2 | FROM ubuntu:20.04 3 | 4 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 5 | 6 | # Set environment variables to prevent interactive prompts during installation 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | # Install necessary tools (git, curl, etc.) 10 | RUN apt-get update && apt-get install -y \ 11 | git \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | # Install dependencies for Docker and Compose 15 | RUN apt-get update && apt-get install -y \ 16 | ca-certificates \ 17 | vim \ 18 | curl \ 19 | gnupg \ 20 | lsb-release 21 | 22 | # Add Docker's official GPG key 23 | RUN mkdir -p /etc/apt/keyrings 24 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 25 | 26 | # Add Docker repository 27 | RUN echo \ 28 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 29 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null 30 | 31 | # Install Docker and Docker Compose plugin 32 | RUN apt-get update && apt-get install -y \ 33 | docker-ce \ 34 | docker-ce-cli \ 35 | containerd.io \ 36 | docker-compose-plugin 37 | 38 | # Verify installation 39 | RUN docker --version && docker compose version 40 | 41 | # Install NodeJS 42 | RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - 43 | RUN apt-get -y install nodejs 44 | RUN npm install -g npm 45 | 46 | # Set the working directory inside the container 47 | WORKDIR /app 48 | 49 | # Copy package files first 50 | COPY package*.json ./ 51 | 52 | # Install dependencies 53 | RUN npm install 54 | 55 | # Copy source files 56 | COPY . . 57 | 58 | # Build the TypeScript project 59 | RUN npm run build 60 | 61 | EXPOSE 8889 62 | 63 | # Make another script executable and run it 64 | RUN chmod +x /app/setup.sh 65 | RUN chmod +x /app/entrypoint.sh 66 | RUN chmod +x /app/watch-container-stop.sh 67 | 68 | RUN echo "source /app/setup.sh" >> ~/.bashrc 69 | 70 | # Command to run at container start 71 | CMD ["/app/watch-container-stop.sh"] 72 | 73 | ENTRYPOINT ["./entrypoint.sh"] 74 | -------------------------------------------------------------------------------- /Dockerfile-arm64: -------------------------------------------------------------------------------- 1 | # Start with a base image that includes git 2 | FROM ubuntu:20.04 3 | 4 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 5 | 6 | # Set environment variables to prevent interactive prompts during installation 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | # Install necessary tools (git, curl, etc.) 10 | RUN apt-get update && apt-get install -y \ 11 | git \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | # Install dependencies for Docker and Compose 15 | RUN apt-get update && apt-get install -y \ 16 | ca-certificates \ 17 | vim \ 18 | curl \ 19 | gnupg \ 20 | lsb-release 21 | 22 | # Add Docker's official GPG key 23 | RUN mkdir -p /etc/apt/keyrings 24 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 25 | 26 | # Add Docker repository 27 | RUN echo \ 28 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 29 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null 30 | 31 | # Install Docker and Docker Compose plugin 32 | RUN apt-get update && apt-get install -y \ 33 | docker-ce \ 34 | docker-ce-cli \ 35 | containerd.io \ 36 | docker-compose-plugin 37 | 38 | # Verify installation 39 | RUN docker --version && docker compose version 40 | 41 | # Install NodeJS 42 | RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - 43 | RUN apt-get -y install nodejs 44 | RUN npm install -g npm 45 | 46 | # Set the working directory inside the container 47 | WORKDIR /app 48 | 49 | # Copy package files first 50 | COPY package*.json ./ 51 | 52 | # Install dependencies 53 | RUN npm install 54 | 55 | # Copy source files 56 | COPY . . 57 | 58 | # Build the TypeScript project 59 | RUN npm run build 60 | 61 | EXPOSE 8889 62 | 63 | # Make another script executable and run it 64 | RUN chmod +x /app/setup.sh 65 | RUN chmod +x /app/entrypoint.sh 66 | RUN chmod +x /app/watch-container-stop.sh 67 | 68 | RUN echo "source /app/setup.sh" >> ~/.bashrc 69 | 70 | # Command to run at container start 71 | CMD ["/app/watch-container-stop.sh"] 72 | 73 | ENTRYPOINT ["./entrypoint.sh"] 74 | -------------------------------------------------------------------------------- /sample-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "org": { 3 | "HZN_ORG_ID": "playground", 4 | "HZN_DEVICE_TOKEN": "", 5 | "HZN_DEVICE_ID": "dragon-fly", 6 | "HZN_EXCHANGE_USER_AUTH": "", 7 | "HZN_EXCHANGE_URL": "http://open-horizon.lfedge.iol.unh.edu:3090/v1", 8 | "HZN_FSS_CSSURL": "http://open-horizon.lfedge.iol.unh.edu:9443/", 9 | "HZN_AGBOT_URL": "http://open-horizon.lfedge.iol.unh.edu:3111", 10 | "HZN_SDO_SVC_URL": "http://open-horizon.lfedge.iol.unh.edu:9008/api", 11 | "HZN_AGENT_PORT": "8510", 12 | "HZN_CSS": true, 13 | "CONFIG_CERT_PATH": "/home/oh/cert/oh/agent-install.crt", 14 | "CONFIG_FILE_PATH": "/home/oh", 15 | "CONFIG_FILE_NAME": "oh-in-container.json", 16 | "ANAX": "https://github.com/open-horizon/anax/releases/download/v2.30.0-1548" 17 | }, 18 | "service": { 19 | "SERVICE_NAME": "mms-agent", 20 | "SERVICE_CONTAINER_NAME": "mms-agent", 21 | "SERVICE_VERSION": "1.0.0", 22 | "SERVICE_VERSION_RANGE_UPPER": "1.0.0", 23 | "SERVICE_VERSION_RANGE_LOWER": "1.0.0", 24 | "SERVICE_CONTAINER_CREDS": "", 25 | "VOLUME_MOUNT": "/mms-shared", 26 | "MMS_SHARED_VOLUME": "mms_shared_volume", 27 | "MMS_OBJECT_TYPE": "mms_agent_config", 28 | "MMS_OBJECT_ID": "mms_agent_config_json", 29 | "MMS_OBJECT_FILE": "config/config.json", 30 | "MMS_CONTAINER_CREDS": "", 31 | "MMS_CONTAINER_NAME": "mms-agent", 32 | "MMS_SERVICE_NAME": "mms-agent", 33 | "MMS_SERVICE_VERSION": "1.0.0", 34 | "MMS_SERVICE_FALLBACK_VERSION": "1.0.0", 35 | "UPDATE_FILE_NAME": "mms-agent-config.json" 36 | }, 37 | "folders": [ 38 | "/var/tmp/horizon/horizon1/fss-domain-socket", 39 | "/var/tmp/horizon/horizon1/ess-auth", 40 | "/var/tmp/horizon/horizon1/secrets", 41 | "/var/tmp/horizon/horizon1/nmp" 42 | ], 43 | "local": { 44 | "YOUR_DOCKERHUB_ID": "", 45 | "DOCKER_REGISTRY": "hub.docker.com", 46 | "DOCKER_TOKEN": "" 47 | }, 48 | "register": { 49 | "policy": { 50 | "properties": [ 51 | { 52 | "name": "openhorizon.allowPrivileged", 53 | "value": true 54 | } 55 | ], 56 | "deployment": { 57 | "properties": [ 58 | {"name": "mms-agent", "value": "MMS Agent"}, 59 | {"name": "open-horizon-bee-stack", "value": "Open Horizon Bee Stack"} 60 | ] 61 | } 62 | } 63 | }, 64 | "test": true, 65 | "anaxInContainer": "docker run -d -t --restart always --name horizon1 --privileged -p 127.0.0.1:8081:8510 -e DOCKER_NAME=horizon1 -e HZN_VAR_RUN_BASE=/var/tmp/horizon/horizon1 -e ANAX_DOCKER_ENDPOINT=unix:///var/run/docker.sock -v /var/run/docker.sock:/var/run/docker.sock -v /var/horizon:/etc/default/horizon:ro -v ${CONFIG_CERT_PATH}:${HZN_MGMT_HUB_CERT_PATH} -v horizon1_var:/var/horizon/ -v horizon1_etc:/etc/horizon/ -v /var/tmp/horizon/horizon1:/var/tmp/horizon/horizon1 openhorizon/arm64_anax:2.30.0-1194", 66 | "cliInContainer": "docker run -d -it --restart always --name auto-dock --privileged --network=\"host\" -v /var/lib/docker/volumes/mms_shared_volume/_data:/mms-shared/ -p 127.0.0.1:8888:8888 -v /var/run/docker.sock:/var/run/docker.sock -v ${CONFIG_CERT_PATH}:${HZN_MGMT_HUB_CERT_PATH} -v ${CONFIG_FILE_PATH}/${CONFIG_FILE_NAME}:/var/${CONFIG_FILE_NAME} -e HORIZON_URL=http://localhost:8081 -e HZN_ORG_ID=${HZN_ORG_ID} -e HZN_EXCHANGE_USER_AUTH=${HZN_EXCHANGE_USER_AUTH} -e HZN_FSS_CSSURL=${HZN_FSS_CSSURL} -e HZN_EXCHANGE_URL=${HZN_EXCHANGE_URL} -e HZN_CONFIG_FILE=/var/${CONFIG_FILE_NAME} -e version=v2.30.0-1291 -e css=${HZN_CSS} playbox21/auto-dock-express_arm64:1.0.6" 67 | } 68 | -------------------------------------------------------------------------------- /src/common/interface/model.ts: -------------------------------------------------------------------------------- 1 | export interface IPayload { 2 | hello: string; 3 | events: IEAMEvent[]; 4 | } 5 | export class LastRun { 6 | timestamp: number; 7 | succeeded: boolean; 8 | 9 | constructor(timestamp = 0, succeeded = false) { 10 | this.succeeded =succeeded; 11 | this.timestamp = timestamp; 12 | } 13 | } 14 | export class IEAMEvent { 15 | title: string = ''; 16 | type: string = ''; 17 | id: string = ''; 18 | action: string = ''; 19 | meta: any = {}; 20 | start: string = ''; 21 | end: string = ''; 22 | frequency: number = 60000; 23 | lastRun: LastRun; 24 | 25 | constructor(event: IEAMEvent) { 26 | this.lastRun = new LastRun(); 27 | Object.assign(this, event) 28 | } 29 | 30 | isValidDate(date: string) { 31 | return !isNaN(Date.parse(date)) 32 | } 33 | getStartTime() { 34 | return new Date(this.start) 35 | } 36 | getEndTime() { 37 | return new Date(this.end) 38 | } 39 | isWithinDateRange() { 40 | if(this.isValidDate(this.start) && this.isValidDate(this.end)) { 41 | const date = new Date() 42 | const start = new Date(this.start) 43 | const end = new Date(this.end) 44 | return date >= start && date <= end 45 | } else { 46 | return false 47 | } 48 | } 49 | isWithinTimeRange() { 50 | if(this.isWithinDateRange()) { 51 | const now = Date.now(); 52 | const start = new Date(this.start).getTime(); 53 | const end = new Date(this.end).getTime(); 54 | return now >= start && now <= end 55 | } else { 56 | return false 57 | } 58 | } 59 | isTimeToRun() { 60 | if(this.isWithinDateRange()) { 61 | return Date.now() - this.lastRun.timestamp > this.frequency 62 | } else { 63 | return false 64 | } 65 | } 66 | isActionAllow() { 67 | return AllowableActions.indexOf(this.action) >= 0 68 | } 69 | isClearToRun() { 70 | return this.isActionAllow() && this.isWithinTimeRange() && this.isTimeToRun() 71 | } 72 | } 73 | 74 | export class EnumClass { 75 | private enum: any = {}; 76 | constructor(private eArray: any[] = []) { 77 | eArray.map((el, idx) => { 78 | this.enum[el] = idx; 79 | }); 80 | } 81 | 82 | getEnum(key: string) { 83 | return this.enum[key]; 84 | } 85 | } 86 | 87 | export const AllowableActions = [ 88 | 'autoRegisterWithPolicy', 'autoRegisterWithPattern', 'autoUnregister', 'autoUpdateNodePolicy' 89 | ] 90 | 91 | export enum Action { 92 | autoUpdateNodePolicy = 0, 93 | autoRegisterWithPolicy = 1, 94 | autoRegisterWithPattern = 2, 95 | autoUnregister = 3 96 | } 97 | 98 | export class EventTime { 99 | hour: number; 100 | minute: number; 101 | second: number; 102 | meriden: 'PM' | 'AM'; 103 | format: 12 | 24; 104 | } 105 | 106 | export interface IProperty { 107 | name: string; 108 | value: string; 109 | } 110 | export interface IConstraint { 111 | constraints: string[] | null; 112 | } 113 | export class PolicyClass { 114 | properties: IProperty[] | null = null; 115 | constraints: IConstraint = null; 116 | } 117 | export class IEAMPolicy { 118 | properties: IProperty[]; 119 | constraints: IConstraint; 120 | deployment: PolicyClass = new PolicyClass(); 121 | managment: PolicyClass = new PolicyClass(); 122 | 123 | constructor(policy: IEAMPolicy) { 124 | Object.assign(this, policy) 125 | } 126 | } 127 | 128 | export const TopLevelDefaultProperties = [ 129 | 'openhorizon.hardwareId', 'openhorizon.operatingSystem', 'openhorizon.containerized', 'openhorizon.cpu', 'openhorizon.arch', 'openhorizon.memory' 130 | ] -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | import express from 'express'; 3 | import fileUpload from 'express-fileupload'; 4 | import path from 'path'; 5 | import { Observable } from 'rxjs'; 6 | 7 | import { Utils } from './common'; 8 | import { HznParams, Params } from './common/params'; 9 | 10 | export const utils = new Utils(); 11 | 12 | export class Server { 13 | params: Params = {}; 14 | app = express(); 15 | apiUrl = `${process.env.SERVERLESS_ENDPOINT}` 16 | constructor(private port = 8889) { 17 | this.initialise() 18 | } 19 | 20 | getParams(params: HznParams) { 21 | return Object.assign(this.params, params) 22 | } 23 | setCorsHeaders(req: express.Request, res: express.Response) { 24 | res.header("Access-Control-Allow-Origin", "YOUR_URL"); // restrict it to the required domain 25 | res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS"); 26 | // Set custom headers for CORS 27 | res.header("Access-Control-Allow-Headers", "Content-type,Accept,X-Custom-Header"); 28 | 29 | } 30 | streamData(req: express.Request, res: express.Response, parse = true) { 31 | let params = this.getParams(req.query as unknown as HznParams); 32 | let body = '' 33 | return new Observable((observer) => { 34 | req.on('data', (data) => { 35 | body += data 36 | }) 37 | .on('close', () => { 38 | try { 39 | // console.log(body) 40 | let data = JSON.parse(body); 41 | if(!parse) { 42 | params.body = data 43 | } else { 44 | Object.keys(data).forEach((key) => { 45 | params[key] = data[key]; 46 | }) 47 | } 48 | observer.next(params) 49 | observer.complete() 50 | } catch(e) { 51 | observer.error(e) 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | initialise() { 58 | let app = this.app; 59 | app.use(cors({ 60 | origin: '*' 61 | })); 62 | app.use(fileUpload()); 63 | 64 | app.use('/static', express.static('public')); 65 | 66 | app.use('/', express.static('dist/bee-stack-dashboard')) 67 | 68 | app.get('/', (req: express.Request, res: express.Response, next: any) => { //here just add next parameter 69 | res.sendFile( 70 | path.resolve(__dirname, "index.html") 71 | ) 72 | // next(); 73 | }) 74 | 75 | app.get("/staff", (req: express.Request, res: express.Response) => { 76 | res.json(["Jeff", "Joe", "John", "Mikio", "Rob", "Sanjeev", "Susan"]); 77 | }); 78 | 79 | app.get("/get_weather_info", (req: express.Request, res: express.Response, next) => { 80 | utils.httpGet(`${this.apiUrl}/get_weather_info`) 81 | .subscribe({ 82 | next: (data: any) => res.send(data), 83 | error: (err: any) => next(err) 84 | }) 85 | }); 86 | 87 | app.get("/get_crop_list", (req: express.Request, res: express.Response, next) => { 88 | utils.httpGet(`${this.apiUrl}/get_crop_list`) 89 | .subscribe({ 90 | next: (data: any) => res.send(data), 91 | error: (err: any) => next(err) 92 | }) 93 | }); 94 | 95 | app.get("/unregister", (req: express.Request, res: express.Response, next) => { 96 | utils.shell(`oh deploy autoUnregister`) 97 | .subscribe({ 98 | next: (data: any) => res.send(data), 99 | error: (err: any) => next(err) 100 | }) 101 | }); 102 | 103 | app.get("/register_with_policy", (req: express.Request, res: express.Response, next) => { 104 | utils.shell(`oh deploy autoRegisterWithPolicy`) 105 | .subscribe({ 106 | next: (data: any) => res.send(data), 107 | error: (err: any) => next(err) 108 | }) 109 | }); 110 | 111 | app.get("/register_with_pattern", (req: express.Request, res: express.Response, next) => { 112 | utils.shell(`oh deploy autoRegisterWithPattern`) 113 | .subscribe({ 114 | next: (data: any) => res.send(data), 115 | error: (err: any) => next(err) 116 | }) 117 | }); 118 | 119 | app.post('/update_config', (req: express.Request, res: express.Response, next) => { 120 | this.streamData(req, res) 121 | .subscribe({ 122 | next: (params: Params) => { 123 | console.log(params) 124 | res.send('test') 125 | //this.cosClient.mkdir(params) 126 | //.subscribe({ 127 | // next: (data: any) => res.send(data), 128 | // error: (err: any) => next(err) 129 | //}) 130 | }, error: (err) => next(err) 131 | }) 132 | }) 133 | 134 | app.listen(this.port, () => { 135 | console.log(`Started on ${this.port}`); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /bee-stack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # shellcheck disable=SC2059 4 | # disable "no variables in printf" due to color codes 5 | 6 | REQUIRED_COMPOSE_VERSION=2.26 7 | ORANGE='\e[1;33m' 8 | BLUE='\e[1;34m' 9 | NC='\e[0m' # No Color 10 | TMP_ENV_FILE=.env.tmp 11 | 12 | set -e 13 | 14 | print_error() { 15 | printf "${ORANGE}❗ERROR: %s${NC}\n" "$1" 16 | shift 17 | [ -n "$1" ] && printf "${ORANGE}❗ %s${NC}\n" "$@" 18 | printf "❗\n${ORANGE}❗Visit the troubleshooting guide:\n" 19 | printf "❗${BLUE}https://github.com/i-am-bee/bee-stack/blob/main/docs/troubleshooting.md${NC}\n" 20 | } 21 | 22 | print_header() { 23 | printf "${BLUE}%s${NC}\n" "$1" 24 | } 25 | 26 | MISSING_COMPOSE_MSG=$(cat << EOF 27 | For installation instructions, see: 28 | - ${BLUE}Podman desktop:${NC} https://podman.io 29 | ⚠️ install using the official installer (not through package manager like brew, apt, etc.) 30 | ⚠️ use rootful machine (default) 31 | ⚠️ use docker compatibility mode 32 | - ${BLUE}Rancher desktop:${NC} https://rancherdesktop.io 33 | - ${BLUE}Docker desktop:${NC} https://www.docker.com/ 34 | EOF 35 | ) 36 | 37 | choose() { 38 | print_header "${1}:" 39 | local range="[1-$(($# - 1))]" 40 | 41 | for ((i=1; i < $#; i++)); do 42 | choice=$((i+1)) 43 | echo "[${i}]: ${!choice}" 44 | done 45 | 46 | if [ -n "${LLM_SELECTED_OPT}" ]; then 47 | echo "LLM_SELECTED_OPT is set to '${LLM_SELECTED_OPT}'. Automatically selecting." 48 | SELECTED_OPT="${LLM_SELECTED_OPT}" 49 | configure_watsonx 50 | else 51 | while true; do 52 | read -rp "Select ${range}: " SELECTED_NUM 53 | if ! [[ "$SELECTED_NUM" =~ ^[0-9]+$ ]]; then print_error "Please enter a valid number"; continue; fi 54 | if [ "$SELECTED_NUM" -lt 1 ] || [ "$SELECTED_NUM" -ge "$#" ]; then 55 | print_error "Number is not in ${range}"; continue; 56 | fi 57 | break 58 | done 59 | 60 | local idx=$((SELECTED_NUM + 1)) 61 | SELECTED_OPT="${!idx}" 62 | fi 63 | } 64 | 65 | ask_yes_no() { 66 | if [ -n "${AUTO_YES_NO}" ]; then 67 | echo "${AUTO_YES_NO}" 68 | [ "${AUTO_YES_NO}" = "yes" ] && echo "yes" || echo "no" 69 | return 70 | fi 71 | 72 | local answer 73 | read -rp "${1} (Y/n): " answer 74 | answer=$(echo "$answer" | tr '[:upper:]' '[:lower:]') 75 | [ "$answer" = "y" ] && echo "yes" || echo "no" 76 | } 77 | 78 | check_docker() { 79 | # Check if docker or podman is installed 80 | local runtime compose_version major minor req_major req_minor 81 | req_major=$(cut -d'.' -f1 <<< ${REQUIRED_COMPOSE_VERSION}) 82 | req_minor=$(cut -d'.' -f2 <<< ${REQUIRED_COMPOSE_VERSION}) 83 | 84 | 85 | local existing_runtimes=() 86 | for runtime in docker podman; do 87 | command -v "$runtime" &>/dev/null && existing_runtimes+=("$runtime") 88 | done 89 | 90 | if [ ${#existing_runtimes[@]} -eq 0 ]; then 91 | print_error "None of the supported container runtimes are installed (docker, rancher, or podman)" 92 | printf "\n${MISSING_COMPOSE_MSG}" 93 | exit 1 94 | fi 95 | 96 | local runtimes_with_compose=() 97 | for runtime in "${existing_runtimes[@]}"; do 98 | "$runtime" compose version --short &>/dev/null && runtimes_with_compose+=("$runtime") 99 | done 100 | 101 | if [ ${#runtimes_with_compose[@]} -eq 0 ]; then 102 | print_error "Compose extension is not installed for any of the existing runtimes: ${existing_runtimes[*]}" 103 | printf "\n${MISSING_COMPOSE_MSG}" 104 | exit 2 105 | fi 106 | 107 | local compose_versions=() 108 | local compose_version_ok=0 109 | for runtime in "${runtimes_with_compose[@]}"; do 110 | compose_version=$("$runtime" compose version --short || echo "compose_not_found") 111 | 112 | major=$(cut -d'.' -f1 <<< "$compose_version") 113 | minor=$(cut -d'.' -f2 <<< "$compose_version") 114 | 115 | compose_versions+=("${runtime} compose ($compose_version)") 116 | if [ "$major" -gt "$req_major" ] || { [ "$major" -eq "$req_major" ] && [ "$minor" -ge "$req_minor" ]; }; then 117 | compose_version_ok=1 118 | break 119 | fi 120 | done 121 | 122 | compose_versions_print=$(IFS=','; echo "${compose_versions[*]}" | sed 's/,/, /g') 123 | 124 | if [ "$compose_version_ok" -eq 0 ]; then 125 | print_error "None of compose command versions meets the required version ${REQUIRED_COMPOSE_VERSION}." \ 126 | "Found versions: ${compose_versions_print}" \ 127 | "Please (re)install a supported runtime" 128 | echo "" 129 | printf "${MISSING_COMPOSE_MSG}\n" 130 | exit 2 131 | fi 132 | RUNTIME="$runtime" 133 | } 134 | 135 | trim() { 136 | local var="${1#"${1%%[![:space:]]*}"}" 137 | echo "${var%"${var##*[![:space:]]}"}" 138 | } 139 | 140 | write_env() { 141 | local default_prompt default_provided value 142 | default_provided=$([ $# -gt 1 ] && echo 1 || echo 0) 143 | current_value="${!1}" # Check if the environment variable is already set 144 | 145 | echo "${1}=$current_value" 146 | 147 | # If the variable exists in the environment, use its value 148 | if [ -n "$current_value" ]; then 149 | value="$current_value" 150 | echo "$1=$value" >> "$TMP_ENV_FILE" 151 | export "${1}=${value}" 152 | return 153 | fi 154 | 155 | # Build the prompt if no existing environment variable is found 156 | default_prompt="$([ "$default_provided" -eq 1 ] && echo " (leave empty for default '${2}')" || echo "")" 157 | 158 | while true; do 159 | read -rp "Provide ${1}${default_prompt}: " value 160 | if [ -z "$value" ] && [ "$default_provided" -eq 0 ]; then 161 | print_error "Value is required" 162 | continue 163 | fi 164 | break 165 | done 166 | value="$([ -z "$value" ] && echo "$2" || echo "$value")" 167 | value="$(trim "$value")" 168 | echo "$1=$value" >> "$TMP_ENV_FILE" 169 | export "${1}=${value}" 170 | } 171 | 172 | write_backend() { 173 | echo LLM_BACKEND="$1" >> "$TMP_ENV_FILE" 174 | echo EMBEDDING_BACKEND="$1" >> "$TMP_ENV_FILE" 175 | } 176 | 177 | configure_bam() { 178 | write_backend bam 179 | write_env BAM_API_KEY 180 | } 181 | 182 | configure_watsonx() { 183 | write_backend watsonx 184 | write_env WATSONX_PROJECT_ID 185 | write_env WATSONX_API_KEY 186 | write_env WATSONX_REGION "us-south" 187 | } 188 | 189 | configure_ollama() { 190 | write_backend ollama 191 | write_env OLLAMA_URL "http://host.docker.internal:11434" 192 | print_header "Checking Ollama connection" 193 | if ! ${RUNTIME} run --rm -it curlimages/curl "$OLLAMA_URL"; then 194 | print_error "Ollama is not running or accessible from containers." 195 | printf " Make sure you configured OLLAMA_HOST=0.0.0.0\n" 196 | printf " see https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server\n" 197 | printf " or run ollama from command line ${BLUE}OLLAMA_HOST=0.0.0.0 ollama serve${NC}\n" 198 | printf " Do not forget to pull the required LLMs ${BLUE}ollama pull llama3.1${NC}\n" 199 | exit 2 200 | fi 201 | } 202 | 203 | configure_openai() { 204 | write_backend openai 205 | write_env OPENAI_API_KEY 206 | } 207 | 208 | setup() { 209 | printf "🐝 Welcome to the bee-stack! You're just a few questions away from building agents!\n(Press ^C to exit)\n\n" 210 | rm -f "$TMP_ENV_FILE" 211 | choose "Choose LLM provider" "watsonx" "ollama" "bam" "openai" 212 | [[ $SELECTED_OPT == 'bam' ]] && configure_bam 213 | [[ $SELECTED_OPT == 'ollama' ]] && configure_ollama 214 | [[ $SELECTED_OPT == 'watsonx' ]] && configure_watsonx 215 | [[ $SELECTED_OPT == 'openai' ]] && configure_openai 216 | 217 | if [ -f ".env" ]; then 218 | [ "$(ask_yes_no ".env file already exists. Do you want to override it?")" = 'no' ] && exit 1 219 | if [ -n "$(${RUNTIME} compose ps -aq)" ]; then 220 | [ "$(ask_yes_no "bee-stack data must be removed when changing configuration, are you sure?")" = 'no' ] && exit 1 221 | clean_stack 222 | fi 223 | fi 224 | 225 | cp "$TMP_ENV_FILE" .env 226 | [ "$(ask_yes_no "Do you want to start bee-stack now?")" = 'yes' ] && start_stack 227 | } 228 | 229 | start_stack() { 230 | if ! [ -f ".env" ]; then 231 | [ "$(ask_yes_no "bee-stack is not yet configured, do you want to configure it now?")" = 'yes' ] && setup || exit 3 232 | fi 233 | 234 | ${RUNTIME} compose --profile all up -d 235 | printf "Done. You can visit the UI at ${BLUE}http://localhost:3000${NC}\n" 236 | } 237 | 238 | stop_stack() { 239 | ${RUNTIME} compose --profile all down 240 | ${RUNTIME} compose --profile infra down 241 | } 242 | 243 | clean_stack() { 244 | ${RUNTIME} compose --profile all down --volumes 245 | ${RUNTIME} compose --profile infra down --volumes 246 | rm -rf tmp 247 | mkdir -p ./tmp/code-interpreter-storage 248 | } 249 | 250 | start_infra() { 251 | mkdir -p ./tmp/code-interpreter-storage 252 | ${RUNTIME} compose --profile infra up -d 253 | } 254 | 255 | dump_logs() { 256 | timestamp=$(date +"%Y-%m-%d_%H%MS") 257 | folder="./logs/${timestamp}" 258 | mkdir -p "${folder}" 259 | 260 | for component in $(${RUNTIME} compose --profile all config --services); do 261 | ${RUNTIME} compose logs "${component}" > "${folder}/${component}.log" 262 | done 263 | 264 | ${RUNTIME} version > "${folder}/${RUNTIME}.log" 265 | ${RUNTIME} compose version > "${folder}/${RUNTIME}.log" 266 | 267 | zip -r "${folder}.zip" "${folder}/"|| echo "Zip is not installed, please upload individual logs" 268 | 269 | printf "${ORANGE}Logs were created in ${folder}${NC}.\n" 270 | printf "If you have issues running bee-stack, please create an issue " 271 | printf "and attach the file ${ORANGE}${folder}.zip${NC} at:\n" 272 | printf "${BLUE}https://github.com/i-am-bee/bee-stack/issues/new?template=run_stack_issue.md${NC}\n" 273 | } 274 | 275 | # Main 276 | check_docker 277 | command=$(trim "$1" | tr '[:upper:]' '[:lower:]') 278 | command=$([ -z "$command" ] && echo "setup" || echo "$command") 279 | if [ "$command" = 'setup' ]; then setup 280 | elif [ "$command" = 'start' ]; then start_stack 281 | elif [ "$command" = 'start:infra' ]; then start_infra 282 | elif [ "$command" = 'stop' ]; then stop_stack 283 | elif [ "$command" = 'clean' ]; then clean_stack 284 | elif [ "$command" = 'check' ]; then check_docker 285 | elif [ "$command" = 'logs' ]; then dump_logs 286 | else print_error "Unknown command $1" 287 | fi 288 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readdirSync } from 'fs'; 2 | import * as https from 'https'; 3 | import jsonfile from 'jsonfile'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Action, IEAMEvent, IEAMPolicy } from './interface/model'; 7 | import { HznParams } from './params/hzn-params'; 8 | 9 | const cp = require('child_process'), 10 | exec = cp.exec; 11 | 12 | export class Utils { 13 | homePath = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 14 | mmsPath = '/mms-shared'; 15 | localPath = './local-shared'; 16 | assets = './assets'; 17 | sharedPath = ''; 18 | intervalMS = 10000; 19 | timer: any; 20 | timestamp = Date.now(); 21 | logTime = Date.now(); 22 | 23 | constructor() { 24 | this.init() 25 | } 26 | init() { 27 | if(!existsSync(this.localPath)) { 28 | mkdirSync(this.localPath); 29 | } 30 | this.resetTimer() 31 | console.log(`check per ${this.intervalMS/1000} sec, console.log per 10 minutes, last checked: ${new Date().toLocaleString()}`) 32 | } 33 | 34 | httpGet(url) { 35 | return new Observable((observer) => { 36 | https.get(url, (resp) => { 37 | let data = ''; 38 | 39 | // A chunk of data has been recieved. 40 | resp.on('data', (chunk) => { 41 | data += chunk; 42 | }); 43 | // The whole response has been received. Print out the result. 44 | resp.on('end', () => { 45 | observer.next(JSON.parse(data)); 46 | observer.complete(); 47 | }); 48 | }).on("error", (err) => { 49 | console.log("Error: " + err.message); 50 | observer.error(err); 51 | }); 52 | }); 53 | } 54 | unregister(params: HznParams) { 55 | return this.shell(`oh deploy autoUnregister`) 56 | } 57 | registerWithPolicy(params: HznParams) { 58 | return this.shell(`oh deploy autoRegisterWithPolicy`) 59 | } 60 | registerWithPattern(params: HznParams) { 61 | return this.shell(`oh deploy autoRegisterWithPattern`) 62 | } 63 | updatePolicy(params: HznParams) { 64 | return this.shell(`oh deploy autoAddpolicy`) 65 | } 66 | checkMMS() : any[] { 67 | try { 68 | let list; 69 | let config; 70 | if(existsSync(this.mmsPath)) { 71 | list = readdirSync(this.mmsPath); 72 | list = list.filter(item => /(\.zip|\.json)$/.test(item)); 73 | this.sharedPath = this.mmsPath; 74 | } else if(existsSync(this.localPath)) { 75 | list = readdirSync(this.localPath); 76 | config = list.filter(item => item === 'config.json'); 77 | list = list.filter(item => /(\.zip|\.json)$/.test(item)); 78 | this.sharedPath = this.localPath; 79 | } 80 | return list; 81 | } catch(e) { 82 | console.log(e) 83 | } 84 | } 85 | resetTimer() { 86 | clearInterval(this.timer); 87 | this.timer = undefined; 88 | this.setInterval(this.intervalMS); 89 | } 90 | setInterval(ms) { 91 | this.timer = setInterval(async () => { 92 | let mmsFiles = this.checkMMS(); 93 | if(Date.now() - this.timestamp > 600000) { 94 | this.timestamp = Date.now(); 95 | console.log(`last checked: ${new Date().toLocaleString()}`) 96 | } else if(Date.now() - this.logTime > 86400000) { 97 | console.clear(); 98 | this.logTime = Date.now(); 99 | } 100 | //console.log('checking', mmsFiles) 101 | if(mmsFiles && mmsFiles.length > 0) { 102 | mmsFiles.forEach(file => { 103 | // For now only handle json files 104 | if(file.indexOf('.json') > 0) { 105 | let arg = `mv ${this.sharedPath}/${file} ${this.assets}/config.json` 106 | this.shell(arg) 107 | .subscribe({ 108 | next: (res) => { 109 | }, 110 | complete: () => { 111 | console.log('complete') 112 | this.doRun(); 113 | }, 114 | error: (err) => { 115 | console.log('error', err) 116 | this.resetTimer() 117 | } 118 | }) 119 | } else { 120 | this.doRun(); 121 | } 122 | }); 123 | } else { 124 | this.doRun(); 125 | } 126 | }, ms); 127 | } 128 | doRun() { 129 | clearInterval(this.timer); 130 | this.runTasks() 131 | .subscribe({ 132 | complete: () => { 133 | this.resetTimer() 134 | }, 135 | error: (err) => { 136 | this.resetTimer() 137 | } 138 | }) 139 | } 140 | isPropsEqual(prop1, prop2) { 141 | try { 142 | let prop3 = {}; 143 | Object.keys(prop2).forEach((key) => { 144 | if(prop2[key] !== null) { 145 | prop3[key] = prop2[key]; 146 | } 147 | }) 148 | console.log(JSON.stringify(prop1)); 149 | console.log(JSON.stringify(prop3)); 150 | //console.log(JSON.stringify(prop1) == JSON.stringify(prop3)); 151 | return JSON.stringify(prop1) == JSON.stringify(prop3); 152 | } catch(e) { 153 | return false; 154 | } 155 | } 156 | isInArray(prop1, prop2) { 157 | try { 158 | let found = false; 159 | prop1.some((p1) => { 160 | let ar = prop2.filter((p2) => JSON.stringify(p2) == JSON.stringify(p1)) 161 | found = ar.length > 0 162 | return found 163 | }) 164 | return found; 165 | } catch(e) { 166 | return false; 167 | } 168 | } 169 | updateConfigJson(cloneEvent: IEAMEvent, eventJson: IEAMEvent[], json: any) { 170 | return new Observable((observer) => { 171 | cloneEvent.lastRun = {timestamp: Date.now(), succeeded: true} 172 | eventJson.push(Object.assign({},cloneEvent)) 173 | json.events = eventJson.slice() 174 | jsonfile.writeFileSync(`${this.assets}/config.json`, json, {spaces: 2}); 175 | observer.next('') 176 | observer.complete() 177 | }) 178 | } 179 | runTasks() { 180 | return new Observable((observer) => { 181 | try { 182 | let eventJson = []; 183 | let json = jsonfile.readFileSync(`${this.assets}/config.json`); 184 | let ieamEvent: IEAMEvent; 185 | let running = false; 186 | let arg = ''; 187 | json.events.forEach((event: IEAMEvent, idx) => { 188 | ieamEvent = new IEAMEvent(event) 189 | const date = new Date() 190 | //console.log(ieamEvent.isWithinDateRange(), ieamEvent.isActionAllow(), ieamEvent.isClearToRun(), date.toUTCString()) 191 | //console.log(ieamEvent) 192 | if(!running && ieamEvent.isClearToRun()) { 193 | console.log('run tasks') 194 | running = true 195 | let cloneEvent = Object.assign({}, ieamEvent) 196 | switch(Action[cloneEvent.action]) { 197 | case Action.autoUpdateNodePolicy: 198 | try { 199 | arg = '' 200 | let policyStr = cloneEvent.meta && cloneEvent.meta.policy ? JSON.stringify(cloneEvent.meta.policy) : '' 201 | if(policyStr.length > 0 && !cloneEvent.lastRun.succeeded) { 202 | this.getNodePolicy(false) 203 | .subscribe({ 204 | next: (config: any) => { 205 | let equals = []; 206 | let policy = cloneEvent.meta.policy; 207 | let newPolicy = new IEAMPolicy(policy) 208 | Object.keys(policy).forEach((key) => { 209 | if(key == 'properties') { 210 | equals.push(this.isInArray(newPolicy[key], config[key])) 211 | } else { 212 | equals.push(this.isPropsEqual(newPolicy[key], config[key])) 213 | } 214 | }) 215 | let res = equals.filter(eq => !eq); 216 | if(res.length > 0) { 217 | policyStr = policyStr.replace(/\"/g, '\\"') 218 | arg = `oh deploy ${cloneEvent.action} --object="${policyStr}"` 219 | 220 | this.shell(arg) 221 | .subscribe({ 222 | complete: () => { 223 | this.updateConfigJson(cloneEvent, eventJson, json) 224 | .subscribe(() => { 225 | observer.next('') 226 | observer.complete() 227 | }) 228 | }, 229 | error: (err) => { 230 | console.log('error', err) 231 | observer.error(err) 232 | } 233 | }) 234 | } else { 235 | console.log('Update is not needed!') 236 | this.updateConfigJson(cloneEvent, eventJson, json) 237 | .subscribe(() => { 238 | observer.next('') 239 | observer.complete() 240 | }) 241 | } 242 | }, 243 | error: (err) => { 244 | console.log('error', err) 245 | observer.error(err) 246 | } 247 | }) 248 | } else { 249 | console.log('Update is not needed!') 250 | this.updateConfigJson(cloneEvent, eventJson, json) 251 | .subscribe(() => { 252 | observer.next('') 253 | observer.complete() 254 | }) 255 | } 256 | } catch(e) { 257 | observer.error(e) 258 | } 259 | break; 260 | case Action.autoRegisterWithPattern: 261 | case Action.autoRegisterWithPolicy: 262 | case Action.autoUnregister: 263 | this.getNodeConfig() 264 | .subscribe({ 265 | next: (config: any) => { 266 | console.log('is configured?', config.configstate, typeof config) 267 | arg = `oh deploy ${cloneEvent.action}` 268 | if(Action[cloneEvent.action] == Action.autoUnregister) { 269 | if(config.configstate.state === 'configured') { 270 | arg = ''; 271 | } 272 | } else { 273 | if(config.configstate.state !== 'configured') { 274 | arg = ''; 275 | } 276 | } 277 | if(arg.length > 0) { 278 | this.shell(arg) 279 | .subscribe({ 280 | complete: () => { 281 | cloneEvent.lastRun = {timestamp: Date.now(), succeeded: true} 282 | eventJson.push(Object.assign({},cloneEvent)) 283 | json.events = eventJson.slice() 284 | jsonfile.writeFileSync(`${this.assets}/config.json`, json, {spaces: 2}); 285 | observer.next('') 286 | observer.complete() 287 | }, 288 | error: (err) => { 289 | console.log('error', err) 290 | observer.error(err) 291 | } 292 | }) 293 | } else { 294 | observer.next('') 295 | observer.complete() 296 | } 297 | }, 298 | error: (err) => observer.error(err) 299 | }) 300 | break; 301 | default: 302 | running = false; 303 | break; 304 | } 305 | } else { 306 | eventJson.push(Object.assign({},ieamEvent)) 307 | } 308 | }); 309 | //console.log(eventJson) 310 | if(!running) { 311 | observer.next('') 312 | observer.complete() 313 | } 314 | } catch(e) { 315 | console.log(e) 316 | observer.error(e) 317 | } 318 | }) 319 | } 320 | getNodePolicy(prnStdout=true) { 321 | return new Observable((observer) => { 322 | let arg = `hzn policy list` 323 | this.shell(arg, "Successfully list policy", "Failed to list policy", prnStdout) 324 | .subscribe({ 325 | next: (res: any) => { 326 | //console.log(typeof res == 'string') 327 | try { 328 | let json = JSON.parse(res) 329 | observer.next(json) 330 | observer.complete() 331 | } catch(e) { 332 | observer.error(e) 333 | } 334 | }, error(e) { 335 | observer.error(e) 336 | } 337 | }) 338 | }) 339 | } 340 | getNodeConfig() { 341 | return new Observable((observer) => { 342 | let arg = `hzn node list` 343 | this.shell(arg, "Successfully list node", "Failed to list node") 344 | .subscribe({ 345 | next: (res: any) => { 346 | console.log(typeof res == 'string') 347 | try { 348 | let json = JSON.parse(res) 349 | console.log(json.configstate.state) 350 | observer.next(json) 351 | observer.complete() 352 | } catch(e) { 353 | observer.error(e) 354 | } 355 | }, error(e) { 356 | observer.error(e) 357 | } 358 | }) 359 | }) 360 | } 361 | shell(arg: string, success='command executed successfully', error='command failed', prnStdout=true, options={maxBuffer: 1024 * 2000}) { 362 | return new Observable((observer) => { 363 | console.log(arg); 364 | if(!prnStdout) { 365 | options = Object.assign(options, {stdio: 'pipe', encoding: 'utf8'}) 366 | } 367 | exec(arg, options, (err: any, stdout: any, stderr: any) => { 368 | if(!err) { 369 | if(prnStdout) { 370 | console.log(stdout); 371 | } 372 | console.log(success); 373 | observer.next(stdout); 374 | observer.complete(); 375 | } else { 376 | console.log(`${error}: ${err}`); 377 | observer.error(err); 378 | } 379 | }) 380 | }); 381 | } 382 | shell2(arg: string, success='command executed successfully', error='command failed', prnStdout=true, options={maxBuffer: 1024 * 2000}) { 383 | return new Observable((observer) => { 384 | console.log(arg); 385 | let child = exec(arg, options, (err: any, stdout: any, stderr: any) => { 386 | if(!err) { 387 | // console.log(stdout); 388 | console.log(success); 389 | observer.next(prnStdout ? stdout : ''); 390 | observer.complete(); 391 | } else { 392 | console.log(`${error}: ${err}`); 393 | observer.error(err); 394 | } 395 | }); 396 | child.stdout.pipe(process.stdout); 397 | child.stdout.on('data', (data) => { 398 | if(data.indexOf(`Run 'hzn agreement list' to view`) > 0) { 399 | console.log(success); 400 | observer.next(prnStdout ? data : ''); 401 | observer.complete(); 402 | } 403 | }) 404 | child.on('data', (data) => { 405 | console.log(data) 406 | }) 407 | }); 408 | } 409 | } --------------------------------------------------------------------------------