ormcp-server 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- README.md +177 -0
- __init__.py +5 -0
- main.py +63 -0
- microservice_ensure.py +337 -0
- ormcp_server-0.6.1.dist-info/METADATA +314 -0
- ormcp_server-0.6.1.dist-info/RECORD +10 -0
- ormcp_server-0.6.1.dist-info/WHEEL +4 -0
- ormcp_server-0.6.1.dist-info/entry_points.txt +2 -0
- ormcp_server-0.6.1.dist-info/licenses/LICENSE +107 -0
- ormcp_server.py +971 -0
README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# ORMCP Server
|
|
2
|
+
|
|
3
|
+
**Make your relational data AI ready**
|
|
4
|
+
|
|
5
|
+
ORMCP Server is a Model Context Protocol (MCP) server that enables AI assistants to interact with relational databases using intuitive, object-oriented operations in JSON format.
|
|
6
|
+
|
|
7
|
+
## What is ORMCP Server?
|
|
8
|
+
|
|
9
|
+
ORMCP Server bridges the gap between AI applications and relational databases. It leverages the Gilhari microservice framework and JDX ORM to provide seamless database access through the standardized MCP protocol.
|
|
10
|
+
|
|
11
|
+
Instead of writing SQL, AI assistants like Claude and Gemini can work with your data using natural language and high-level object operations.
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
|
|
15
|
+
- **๐ Database Agnostic** - Works with any JDBC-compliant database (PostgreSQL, MySQL, Oracle, SQL Server, SQLite, etc.)
|
|
16
|
+
- **โ
MCP Compliant** - Fully compatible with the Model Context Protocol standard
|
|
17
|
+
- **๐ Object-Relational Mapping** - JSON object operations transparently mapped to relational data
|
|
18
|
+
- **๐งพ Declarative ORM** - Define mappings using simple JDX grammar
|
|
19
|
+
- **๐ High Performance** - Optimized with connection pooling, caching, and minimal database trips
|
|
20
|
+
- **๐ Secure** - Domain model-specific operations with optional read-only mode
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install ormcp-server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Existing Gemfury beta users:** If you previously installed via Gemfury, you can continue using that channel or switch to PyPI. Both are supported during the beta period.
|
|
31
|
+
|
|
32
|
+
### Accessing Full Package with SDK and Examples
|
|
33
|
+
|
|
34
|
+
The source distribution includes everything you need to get started beyond the
|
|
35
|
+
installable package itself โ a curated subset of the Gilhari SDK and a
|
|
36
|
+
ready-to-use example microservice.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Download source distribution from PyPI
|
|
40
|
+
pip download --no-binary :all: ormcp-server
|
|
41
|
+
|
|
42
|
+
# Extract it (use the appropriate version number)
|
|
43
|
+
tar -xzf ormcp_server-*.tar.gz
|
|
44
|
+
cd ormcp_server-*/
|
|
45
|
+
|
|
46
|
+
# Now you have access to:
|
|
47
|
+
# - Gilhari_SDK/ Gilhari SDK essentials (libs, config, docs)
|
|
48
|
+
# Note: JDX User Manual excluded due to size; available at
|
|
49
|
+
# https://www.softwaretree.com/v1/products/jdx/docs/JDX-5.0-Manual-v1.pdf
|
|
50
|
+
# - gilhari_example1/ Ready-to-use SQLite-backed example microservice
|
|
51
|
+
# - package/client/ Example MCP client code
|
|
52
|
+
# - package/docs/ Additional ORMCP documentation
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Existing Gemfury beta users:**
|
|
56
|
+
```bash
|
|
57
|
+
pip download --no-binary :all: \
|
|
58
|
+
--index-url https://YOUR_TOKEN@pypi.fury.io/softwaretree/ \
|
|
59
|
+
--extra-index-url https://pypi.org/simple \
|
|
60
|
+
ormcp-server
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Basic Usage
|
|
64
|
+
|
|
65
|
+
1. **Configure and start your Gilhari microservice** (handles database connectivity)
|
|
66
|
+
2. **Configure ORMCP Server:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export GILHARI_BASE_URL="http://localhost:80/gilhari/v1/"
|
|
70
|
+
export MCP_SERVER_NAME="MyORMCPServer"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. **Start the server:**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
ormcp-server
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. **Connect your AI client** (e.g., Claude Desktop):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"my-ormcp-server": {
|
|
85
|
+
"command": "ormcp-server",
|
|
86
|
+
"env": {
|
|
87
|
+
"GILHARI_BASE_URL": "http://localhost:80/gilhari/v1/",
|
|
88
|
+
"MCP_SERVER_NAME": "MyORMCPServer"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## How It Works
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
AI Assistant <--MCP--> ORMCP Server <--REST--> Gilhari Microservice <--JDBC--> Database
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
1. User sends natural language request to AI assistant (e.g., Claude)
|
|
102
|
+
2. AI assistant translates natural language into MCP tool calls
|
|
103
|
+
3. ORMCP Server receives MCP tool calls and translates to REST API calls
|
|
104
|
+
4. Gilhari microservice executes database operations via JDBC/SQL
|
|
105
|
+
5. Results returned as JSON objects through the chain back to user
|
|
106
|
+
|
|
107
|
+
## Example Interactions
|
|
108
|
+
|
|
109
|
+
**Query data:**
|
|
110
|
+
```
|
|
111
|
+
"Show me all users in California"
|
|
112
|
+
"What's the average age of users?"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Manage data:**
|
|
116
|
+
```
|
|
117
|
+
"Create a new user named John Smith in Boston"
|
|
118
|
+
"Update all users in New York to have status=active"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Explore schema:**
|
|
122
|
+
```
|
|
123
|
+
"What types of objects are available?"
|
|
124
|
+
"Show me the attributes of the User class"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Available MCP Tools
|
|
128
|
+
|
|
129
|
+
- **getObjectModelSummary** - Discover classes and relationships
|
|
130
|
+
- **query** - Retrieve objects with filtering and sorting
|
|
131
|
+
- **getObjectById** - Fetch specific objects by primary key
|
|
132
|
+
- **access** - Navigate object relationships
|
|
133
|
+
- **getAggregate** - Calculate COUNT, SUM, AVG, MIN, MAX
|
|
134
|
+
- **insert** - Create new objects
|
|
135
|
+
- **update** - Update existing objects
|
|
136
|
+
- **update2** - Bulk-update existing objects
|
|
137
|
+
- **delete** - Delete objects
|
|
138
|
+
- **delete2** - Bulk-delete existing objects
|
|
139
|
+
|
|
140
|
+
In the READONLY mode, the insert/update/update2/delete/delete2 tools are not available.
|
|
141
|
+
|
|
142
|
+
## Requirements
|
|
143
|
+
|
|
144
|
+
- Python 3.12+
|
|
145
|
+
- Docker (for Gilhari microservice)
|
|
146
|
+
- JDBC driver for your database
|
|
147
|
+
- MCP-compatible client (Claude Desktop, Gemini CLI, etc.)
|
|
148
|
+
|
|
149
|
+
## Documentation
|
|
150
|
+
|
|
151
|
+
- **Complete Documentation**: [github.com/softwaretree/ormcp-docs](https://github.com/softwaretree/ormcp-docs)
|
|
152
|
+
- **API Reference**: [ORMCP Tools Reference](https://github.com/softwaretree/ormcp-docs/blob/main/reference/ormcp_tools_reference.md)
|
|
153
|
+
- **Quick Start**: [Getting Started Guide](https://github.com/softwaretree/ormcp-docs#quick-start)
|
|
154
|
+
- **GitHub repositories with working examples**: [Browse Examples](https://github.com/softwaretree/ormcp-docs/tree/main/examples)
|
|
155
|
+
- **Example Microservice**: [github.com/SoftwareTree/gilhari_example1](https://github.com/SoftwareTree/gilhari_example1)
|
|
156
|
+
|
|
157
|
+
## Beta Notice
|
|
158
|
+
|
|
159
|
+
ORMCP Server is currently in Beta for testing and evaluation purposes only. Not intended for commercial use.
|
|
160
|
+
|
|
161
|
+
## Support
|
|
162
|
+
|
|
163
|
+
- **Email**: ormcp_support@softwaretree.com
|
|
164
|
+
- **Issues**: [github.com/softwaretree/ormcp-docs/issues](https://github.com/softwaretree/ormcp-docs/issues)
|
|
165
|
+
- **Website**: [softwaretree.com](https://www.softwaretree.com)
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
Proprietary software owned by Software Tree, LLC. See LICENSE for terms.
|
|
170
|
+
|
|
171
|
+
**Beta Evaluation**: Free for testing during evaluation period.
|
|
172
|
+
|
|
173
|
+
**Commercial licensing** terms will be announced at the time of commercial release. For information or to express interest, contact Software Tree at [ormcp_support@softwaretree.com](mailto:ormcp_support@softwaretree.com) or visit [https://www.softwaretree.com](https://www.softwaretree.com).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
**Software Tree** | Making databases AI-accessible
|
__init__.py
ADDED
main.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright (c) 2025, Software Tree
|
|
2
|
+
|
|
3
|
+
# Alternate entry point for running the ORMCP server in dev/test modes.
|
|
4
|
+
|
|
5
|
+
# Author: Damodar Periwal
|
|
6
|
+
"""
|
|
7
|
+
Alternate entry point for running the ORMCP server in dev/test/http modes.
|
|
8
|
+
Example: python main.py --mode http --port 8080
|
|
9
|
+
"""
|
|
10
|
+
# main.py
|
|
11
|
+
|
|
12
|
+
# !/usr/bin/env python3
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from ormcp_server import run_server, ORMCP_SERVER_VERSION
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="ORMCP Dev/Test Launcher")
|
|
20
|
+
parser.add_argument("--version", action="version",
|
|
21
|
+
version=f"ORMCP Server v{ORMCP_SERVER_VERSION}")
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--mode",
|
|
25
|
+
choices=["dev", "test", "stdio", "http"],
|
|
26
|
+
default="dev",
|
|
27
|
+
help="Run mode: dev = stdio, test = mock, stdio = explicit stdio mode, http = HTTP server"
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--host", default="127.0.0.1",
|
|
31
|
+
help="Host to bind for HTTP mode (ignored for stdio)"
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--port", type=int, default=8080,
|
|
35
|
+
help="Port to bind for HTTP mode (ignored for stdio)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
if args.mode == "dev":
|
|
41
|
+
print("๐จ๐ป Running in development (stdio) mode...", file=sys.stderr)
|
|
42
|
+
sys.stderr.flush()
|
|
43
|
+
run_server("stdio")
|
|
44
|
+
elif args.mode == "test":
|
|
45
|
+
print("๐งช Running in test mode... (placeholder)", file=sys.stderr)
|
|
46
|
+
# Insert mock or testing logic here
|
|
47
|
+
print("Simulated test complete.", file=sys.stderr)
|
|
48
|
+
sys.stderr.flush()
|
|
49
|
+
elif args.mode == "stdio":
|
|
50
|
+
print("๐ Explicit stdio mode...", file=sys.stderr)
|
|
51
|
+
sys.stderr.flush()
|
|
52
|
+
run_server("stdio")
|
|
53
|
+
elif args.mode == "http":
|
|
54
|
+
print(f"๐ Running in HTTP mode on {args.host}:{args.port}...", file=sys.stderr)
|
|
55
|
+
sys.stderr.flush()
|
|
56
|
+
run_server("http", args.host, args.port)
|
|
57
|
+
else:
|
|
58
|
+
print(f"โ ๏ธ Unknown mode: {args.mode}", file=sys.stderr)
|
|
59
|
+
sys.stderr.flush()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
microservice_ensure.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Copyright (c) 2025, Software Tree
|
|
2
|
+
# A utility to check the availability of an app-specific GIlhari microservice.
|
|
3
|
+
# Author: Damodar Periwal
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
# microservice_ensure.py
|
|
7
|
+
|
|
8
|
+
# A utility to check the availability of an app-specific GIlhari microservice.
|
|
9
|
+
# If the Gilhari microservice is not running on not responsive then this
|
|
10
|
+
# utility tries to start the service (through Docker) using the relevant
|
|
11
|
+
# parameter values read from environment variables as described below:
|
|
12
|
+
|
|
13
|
+
Env variables | description | default value
|
|
14
|
+
|
|
15
|
+
"GILHARI_NAME" | Name of the app-specific Gilhari microservice | "my-gilhari-microservice"
|
|
16
|
+
"GILHARI_HOST" | IP address of the host machine | "localhost"
|
|
17
|
+
"GILHARI_PORT" | Port number to contact the Gilhari microservice | 80
|
|
18
|
+
"GILHARI_IMAGE" | Docker image name of the app-specific Gilhari ! ""
|
|
19
|
+
microservice
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
import requests
|
|
23
|
+
import socket
|
|
24
|
+
import subprocess
|
|
25
|
+
import time
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Optional, List
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
# Configure logging
|
|
31
|
+
logging.basicConfig(level=logging.INFO)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ServiceConfig:
|
|
37
|
+
"""Configuration for the microservice"""
|
|
38
|
+
name: str = ""
|
|
39
|
+
host: str = "localhost"
|
|
40
|
+
port: int = 80
|
|
41
|
+
health_endpoint: Optional[str] = "/health"
|
|
42
|
+
test_endpoint: Optional[str] = "/api/status"
|
|
43
|
+
docker_image: str = ""
|
|
44
|
+
docker_run_args: Optional[List[str]] = None
|
|
45
|
+
container_name: Optional[str] = None
|
|
46
|
+
startup_timeout: int = 60
|
|
47
|
+
health_check_interval: int = 2
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
if self.docker_run_args is None:
|
|
51
|
+
self.docker_run_args = []
|
|
52
|
+
if self.container_name is None:
|
|
53
|
+
self.container_name = f"{self.name}-container"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_health_endpoint(config: ServiceConfig) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Method 1: Try health endpoint
|
|
59
|
+
"""
|
|
60
|
+
if not config.health_endpoint:
|
|
61
|
+
logger.debug(f"No health endpoint configured for {config.name}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
url = f"http://{config.host}:{config.port}{config.health_endpoint}"
|
|
66
|
+
logger.debug(f"Checking health endpoint: {url}")
|
|
67
|
+
|
|
68
|
+
response = requests.get(url, timeout=5)
|
|
69
|
+
if response.status_code == 200:
|
|
70
|
+
logger.info(f"Health endpoint check passed for {config.name}")
|
|
71
|
+
return True
|
|
72
|
+
else:
|
|
73
|
+
logger.debug(f"Health endpoint returned status {response.status_code}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
except requests.exceptions.RequestException as e:
|
|
77
|
+
logger.debug(f"Health endpoint check failed: {e}")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def check_rest_api_endpoint(config: ServiceConfig) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Method 2: Try if REST API service responds to a simple endpoint
|
|
84
|
+
"""
|
|
85
|
+
if not config.test_endpoint:
|
|
86
|
+
logger.debug(f"No test endpoint configured for {config.name}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
url = f"http://{config.host}:{config.port}{config.test_endpoint}"
|
|
91
|
+
logger.debug(f"Checking REST API endpoint: {url}")
|
|
92
|
+
|
|
93
|
+
response = requests.get(url, timeout=5)
|
|
94
|
+
|
|
95
|
+
# Option 1: Accept any HTTP response (service is running)
|
|
96
|
+
# This treats 404, 500, etc. as "service is up"
|
|
97
|
+
logger.info(f"REST API endpoint check passed for {config.name} (status: {response.status_code})")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Option 2: Only accept successful responses (uncomment if you want this behavior)
|
|
101
|
+
# if 200 <= response.status_code < 400:
|
|
102
|
+
# logger.info(f"REST API endpoint check passed for {config.name} (status: {response.status_code})")
|
|
103
|
+
# return True
|
|
104
|
+
# else:
|
|
105
|
+
# logger.debug(f"REST API endpoint returned error status: {response.status_code}")
|
|
106
|
+
# return False
|
|
107
|
+
|
|
108
|
+
except requests.exceptions.RequestException as e:
|
|
109
|
+
logger.debug(f"REST API endpoint check failed: {e}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check_port_connection(config: ServiceConfig) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Method 3: Try port connection
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
logger.debug(f"Checking port connection: {config.host}:{config.port}")
|
|
119
|
+
|
|
120
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
121
|
+
sock.settimeout(5)
|
|
122
|
+
result = sock.connect_ex((config.host, config.port))
|
|
123
|
+
sock.close()
|
|
124
|
+
|
|
125
|
+
if result == 0:
|
|
126
|
+
logger.info(f"Port connection check passed for {config.name}")
|
|
127
|
+
return True
|
|
128
|
+
else:
|
|
129
|
+
logger.debug(f"Port connection failed with result: {result}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.debug(f"Port connection check failed: {e}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def is_service_available(config: ServiceConfig) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Test all three methods to check if service is available
|
|
140
|
+
"""
|
|
141
|
+
logger.info(f"Checking availability of {config.name} service...")
|
|
142
|
+
|
|
143
|
+
# Method 1: Health endpoint
|
|
144
|
+
if check_health_endpoint(config):
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
# Method 2: REST API endpoint
|
|
148
|
+
if check_rest_api_endpoint(config):
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Method 3: Port connection
|
|
152
|
+
if check_port_connection(config):
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
logger.info(f"Service {config.name} is not available")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def stop_existing_container(config: ServiceConfig) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Stop and remove existing container if it exists
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Ensure container_name is not None
|
|
165
|
+
container_name = config.container_name or f"{config.name}-container"
|
|
166
|
+
|
|
167
|
+
# Check if container exists
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
["docker", "ps", "-a", "--filter", f"name={container_name}", "--format", "{{.Names}}"],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
check=True
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Fix 1: Handle potential None result from stdout and container_name
|
|
176
|
+
if result.stdout and config.container_name and config.container_name in result.stdout:
|
|
177
|
+
logger.info(f"Stopping existing container: {config.container_name}")
|
|
178
|
+
|
|
179
|
+
# Stop container
|
|
180
|
+
subprocess.run(
|
|
181
|
+
["docker", "stop", str(config.container_name)],
|
|
182
|
+
check=True
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Remove container
|
|
186
|
+
subprocess.run(
|
|
187
|
+
["docker", "rm", str(config.container_name)],
|
|
188
|
+
check=True
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
logger.info(f"Removed existing container: {config.container_name}")
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
except subprocess.CalledProcessError as e:
|
|
195
|
+
logger.warning(f"Error managing existing container: {e}")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def start_docker_service(config: ServiceConfig) -> bool:
|
|
202
|
+
"""
|
|
203
|
+
Start the microservice using Docker
|
|
204
|
+
"""
|
|
205
|
+
if not config.docker_image:
|
|
206
|
+
logger.error(f"No Docker image specified for {config.name}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Stop existing container if it exists
|
|
211
|
+
stop_existing_container(config)
|
|
212
|
+
|
|
213
|
+
# Build Docker run command
|
|
214
|
+
docker_cmd = [
|
|
215
|
+
"docker", "run",
|
|
216
|
+
"-d", # Run in detached mode
|
|
217
|
+
"--name", config.container_name
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Add any additional Docker run arguments
|
|
221
|
+
docker_cmd.extend(config.docker_run_args or [])
|
|
222
|
+
|
|
223
|
+
# Add the image name
|
|
224
|
+
docker_cmd.append(config.docker_image)
|
|
225
|
+
|
|
226
|
+
logger.info(f"Starting Docker container for {config.name}: {' '.join(docker_cmd)}")
|
|
227
|
+
|
|
228
|
+
# Start the container
|
|
229
|
+
result = subprocess.run(docker_cmd, capture_output=True, text=True, check=True)
|
|
230
|
+
container_id = result.stdout.strip()
|
|
231
|
+
|
|
232
|
+
logger.info(f"Started container {config.container_name} with ID: {container_id}")
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
except subprocess.CalledProcessError as e:
|
|
236
|
+
logger.error(f"Failed to start Docker container: {e}")
|
|
237
|
+
logger.error(f"stderr: {e.stderr}")
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def wait_for_service(config: ServiceConfig) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
Wait for the service to become available
|
|
244
|
+
"""
|
|
245
|
+
logger.info(f"Waiting for {config.name} to become available...")
|
|
246
|
+
|
|
247
|
+
start_time = time.time()
|
|
248
|
+
while time.time() - start_time < config.startup_timeout:
|
|
249
|
+
if is_service_available(config):
|
|
250
|
+
elapsed = time.time() - start_time
|
|
251
|
+
logger.info(f"Service {config.name} is now available (took {elapsed:.1f}s)")
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
logger.debug(f"Service not ready yet, waiting {config.health_check_interval}s...")
|
|
255
|
+
time.sleep(config.health_check_interval)
|
|
256
|
+
|
|
257
|
+
logger.error(f"Timeout waiting for {config.name} to become available")
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def ensure_microservice(config: ServiceConfig) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Main function to ensure microservice is running and available
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
config: ServiceConfig object with service configuration
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
bool: True if service is available, False otherwise
|
|
270
|
+
"""
|
|
271
|
+
logger.info(f"Ensuring microservice {config.name} is available...")
|
|
272
|
+
|
|
273
|
+
# Check if service is already available
|
|
274
|
+
if is_service_available(config):
|
|
275
|
+
logger.info(f"Service {config.name} is already available")
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
# Service is not available, try to start it
|
|
279
|
+
logger.info(f"Service {config.name} is not available, starting Docker container...")
|
|
280
|
+
|
|
281
|
+
if not start_docker_service(config):
|
|
282
|
+
logger.error(f"Failed to start Docker service for {config.name}")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
# Wait for service to become available
|
|
286
|
+
if not wait_for_service(config):
|
|
287
|
+
logger.error(f"Service {config.name} failed to become available after startup")
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
logger.info(f"Successfully ensured {config.name} is available")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Ensure that the app-specific Gilhari microservice is running
|
|
295
|
+
def ensure_gilhari_service() -> bool:
|
|
296
|
+
"""
|
|
297
|
+
Gather app-specific Gilhari microservice parameters and ensure its availability
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
GILHARI_INTERNAL_PORT = "8081" # fixed port number of Gilhari inside the container
|
|
301
|
+
portStr = os.getenv("GILHARI_PORT", "80") # Outside port number the container is listening on
|
|
302
|
+
|
|
303
|
+
# First gather the Gilhari microservice parameters
|
|
304
|
+
gilhari_service = ServiceConfig(
|
|
305
|
+
name=os.getenv("GILHARI_NAME", "my-gilhari-microservice"),
|
|
306
|
+
host=os.getenv("GILHARI_HOST", "localhost"),
|
|
307
|
+
port=int(portStr),
|
|
308
|
+
health_endpoint="/gilhari/v1/health/check",
|
|
309
|
+
test_endpoint="/gilhari/v1/api/status",
|
|
310
|
+
docker_image=os.getenv("GILHARI_IMAGE", ""), # don't want any default docker image name to cause confusion
|
|
311
|
+
docker_run_args=["-p", f"{portStr}:{GILHARI_INTERNAL_PORT}"],
|
|
312
|
+
startup_timeout=30,
|
|
313
|
+
health_check_interval=3
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if not ensure_microservice(gilhari_service):
|
|
317
|
+
logger.error(f"Failed to ensure {gilhari_service.name} is available")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Utility functions for testing individual components
|
|
324
|
+
def test_service_methods(config: ServiceConfig):
|
|
325
|
+
"""
|
|
326
|
+
Test individual service checking methods
|
|
327
|
+
"""
|
|
328
|
+
print(f"\nTesting service checking methods for {config.name}:")
|
|
329
|
+
print(f"Health endpoint check: {check_health_endpoint(config)}")
|
|
330
|
+
print(f"REST API endpoint check: {check_rest_api_endpoint(config)}")
|
|
331
|
+
print(f"Port connection check: {check_port_connection(config)}")
|
|
332
|
+
print(f"Overall availability: {is_service_available(config)}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
# Check for the availability of the Gilhari microservice
|
|
337
|
+
ensure_gilhari_service()
|