Skip to content

Commit 41575ce

Browse files
authored
Merge pull request #341 from cooling-singapore/v0.2.0-alpha
V0.2.0 alpha
2 parents 833e8f7 + 285c6a6 commit 41575ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+9779
-6507
lines changed

.dockerignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
__pycache__
2+
*.pyc
3+
*.pyo
4+
*.pyd
5+
.Python
6+
env
7+
pip-log.txt
8+
pip-delete-this-directory.txt
9+
.tox
10+
.coverage
11+
.coverage.*
12+
.cache
13+
nosetests.xml
14+
coverage.xml
15+
*.cover
16+
*.log
17+
.git
18+
.mypy_cache
19+
.pytest_cache
20+
.hypothesis
21+
venv
22+
.venv
23+
24+
docker-compose.yml
25+
Dockerfile

.github/workflows/workflow.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Pull Request Tests
2+
3+
on: pull_request
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v3
11+
12+
- name: Set up Python 3.x
13+
uses: actions/setup-python@v4
14+
with:
15+
python-version: "3.9"
16+
17+
- name: Install application
18+
run: pip install .
19+
20+
- name: Create credentials.json
21+
run: >
22+
echo
23+
'{"github-credentials":[
24+
{"repository":"https://github.com/cooling-singapore/saas-middleware-sdk",
25+
"login":"oauth",
26+
"personal_access_token":"${{ secrets.TEST_PROCESSOR_KEY }}"}
27+
]}' > ~/.saas-credentials.json
28+
29+
- name: Run tests
30+
run: pytest ./tests

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM python:3.9-slim
2+
3+
ENV NODE_TYPE=full
4+
ENV STRICT=true
5+
6+
RUN apt update && apt install -y git
7+
8+
COPY . /saas-mw
9+
RUN pip install /saas-mw
10+
11+
ENTRYPOINT exec saas-node --keystore /keystore --keystore-id $KEYSTORE_ID --password $PASSWORD \
12+
--log-path /logs/node.log \
13+
run --datastore /datastore --rest-address $REST_ADDRESS --p2p-address $P2P_ADDRESS --boot-node $BOOT_NODE \
14+
--type $NODE_TYPE --bind-all-address \
15+
$([ "$STRICT" = "false" ] && echo "--disable-strict-deployment")

docker-compose.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
version: "3.9"
2+
services:
3+
boot_node:
4+
build: .
5+
# network_mode "host" does not work on mac, need to map ports manually
6+
# network_mode: "host"
7+
ports:
8+
- "5001:5001"
9+
- "4001:4001"
10+
environment:
11+
# TODO: Use docker secrets for password
12+
PASSWORD: 1234
13+
KEYSTORE_ID: hnx0rxlhv2bsovj65xu2w4oz682xbeo1hfakad3jhheh2qzlbfm01nq7w38vcauz
14+
REST_ADDRESS: boot_node:5001
15+
P2P_ADDRESS: boot_node:4001
16+
BOOT_NODE: boot_node:4001
17+
# keystore and datastore are found in /keystore and /datastore of the container
18+
volumes:
19+
- ${HOME}/.keystore:/keystore
20+
- ${HOME}/.datastore:/datastore
21+
- ${HOME}/.logs/boot:/logs
22+
node_2:
23+
build: .
24+
ports:
25+
- "5002:5002"
26+
- "4002:4002"
27+
environment:
28+
PASSWORD: 1234
29+
KEYSTORE_ID: qpjyum07zdt1lipsg2trt0jnsb0ysies49pa8felbs56lfxir57ofjdddw2eex9g
30+
REST_ADDRESS: node_2:5002
31+
P2P_ADDRESS: node_2:4002
32+
BOOT_NODE: boot_node:4001
33+
STRICT: false
34+
volumes:
35+
- ${HOME}/.keystore:/keystore
36+
- ${HOME}/.datastore2:/datastore
37+
- ${HOME}/.logs/node2:/logs
38+
depends_on:
39+
- boot_node

requirements.txt

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,19 @@
1-
appnope==0.1.3
2-
asttokens==2.0.7
3-
attrs==22.1.0
4-
backcall==0.2.0
5-
canonicaljson==1.6.2
6-
certifi==2022.6.15
7-
cffi==1.15.1
8-
charset-normalizer==2.1.0
9-
click==8.1.3
10-
cryptography==37.0.4
11-
cypari==2.4.1
12-
decorator==5.1.1
13-
docker==5.0.3
14-
executing==0.9.1
15-
Flask==2.2.2
16-
Flask-Cors==3.0.10
17-
future==0.18.2
18-
FXrays==1.3.5
19-
greenlet==1.1.2
20-
idna==3.3
21-
importlib-metadata==4.12.0
22-
ipython==8.4.0
23-
itsdangerous==2.1.2
24-
jedi==0.18.1
25-
Jinja2==3.1.2
26-
jsonschema==4.9.1
27-
knot-floer-homology==1.2
28-
MarkupSafe==2.1.1
29-
matplotlib-inline==0.1.3
30-
networkx==2.8.5
31-
parso==0.8.3
32-
pexpect==4.8.0
33-
pickleshare==0.7.5
34-
plink==2.4.1
35-
prompt-toolkit==3.0.30
36-
ptyprocess==0.7.0
37-
pure-eval==0.2.2
38-
pycparser==2.21
39-
pydantic==1.9.2
40-
Pygments==2.12.0
41-
pypng==0.20220715.0
42-
pyrsistent==0.18.1
43-
python-snappy==0.6.1
44-
requests==2.28.1
45-
simplejson==3.17.6
46-
six==1.16.0
47-
snappy==3.0.3
48-
snappy-manifolds==1.1.2
49-
spherogram==2.1
50-
SQLAlchemy==1.4.40
51-
stack-data==0.3.0
52-
traitlets==5.3.0
53-
typing_extensions==4.3.0
54-
urllib3==1.26.11
55-
wcwidth==0.2.5
56-
websocket-client==1.3.3
57-
Werkzeug==2.2.2
58-
zipp==3.8.1
1+
jsonschema~=4.17.0
2+
requests~=2.28.1
3+
pydantic~=1.10.2
4+
fastapi~=0.86.0
5+
SQLAlchemy~=1.4.41
6+
cryptography~=38.0.1
7+
starlette~=0.20.4
8+
canonicaljson~=1.6.3
9+
setuptools~=65.5.1
10+
sqlalchemy-json~=0.5.0
11+
python-snappy~=0.6.1
12+
uvicorn~=0.19.0
13+
tabulate~=0.9.0
14+
inquirerpy~=0.3.4
15+
docker~=6.0.0
16+
paramiko~=2.12.0
17+
pyyaml~=6.0.0
18+
python-multipart~=0.0.5
19+
pytest~=7.2.0

saas/_meta.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__version__ = '0.2.0-alpha'
2+
__name__ = 'saas-middleware'
3+
__title__ = 'SaaS Middleware'
4+
__description__ = 'Middleware for powering digital twin federations of models'

saas/cli/cmd_compose.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import os.path
2+
import signal
3+
import time
4+
from enum import Enum
5+
from typing import List, Dict
6+
7+
import yaml
8+
from pydantic import BaseModel
9+
10+
from saas.cli.helpers import CLICommand, Argument, extract_address
11+
from saas.core.keystore import Keystore
12+
from saas.dor.proxy import DORProxy
13+
from saas.dor.service import fetch_proc_descriptor, _generate_gpp_hash
14+
from saas.node import Node
15+
from saas.rti.proxy import RTIProxy
16+
from saas.service import SignalListener
17+
18+
19+
class ProcInfo(BaseModel):
20+
name: str
21+
deployment: str = "native"
22+
ssh_credentials: str = None
23+
24+
25+
class NodeType(str, Enum):
26+
full = 'full'
27+
storage = 'storage'
28+
execution = 'execution'
29+
30+
31+
class DeploySpec(BaseModel):
32+
class NodeSpec(BaseModel):
33+
datastore_path: str = "~/.datastore"
34+
keystore_path: str = "~/.keystore"
35+
log_path: str = "~/.log"
36+
keystore_id: str
37+
password: str
38+
rest_address: str
39+
p2p_address: str
40+
boot_node_address: str
41+
type: NodeType = "full"
42+
processors: List[ProcInfo]
43+
44+
class ProcessorSpec(BaseModel):
45+
source: str
46+
# TODO: Allow commit_id to be None as latest commit. Let node figure it out.
47+
commit_id: str
48+
proc_path: str
49+
proc_config: str
50+
dor: str = None
51+
52+
nodes: Dict[str, NodeSpec]
53+
processors: Dict[str, ProcessorSpec]
54+
55+
56+
def startup_node(node_spec: DeploySpec.NodeSpec):
57+
# create node and startup services
58+
keystore_path = os.path.join(os.path.expanduser(os.path.expanduser(node_spec.keystore_path)),
59+
f"{node_spec.keystore_id}.json")
60+
datastore_path = os.path.expanduser(os.path.expanduser(node_spec.datastore_path))
61+
62+
keystore = Keystore.load(keystore_path, node_spec.password)
63+
node = Node.create(keystore=keystore,
64+
storage_path=datastore_path,
65+
p2p_address=extract_address(node_spec.p2p_address),
66+
boot_node_address=extract_address(node_spec.boot_node_address),
67+
rest_address=extract_address(node_spec.rest_address),
68+
enable_dor=node_spec.type == 'full' or node_spec.type == 'storage',
69+
enable_rti=node_spec.type == 'full' or node_spec.type == 'storage')
70+
71+
return node
72+
73+
74+
def deploy_processors(spec: DeploySpec, nodes: dict[str, Node]):
75+
for node_key, node_spec in spec.nodes.items():
76+
node_address = extract_address(node_spec.rest_address)
77+
rti = RTIProxy(node_address)
78+
79+
keystore = nodes[node_key].keystore
80+
81+
for proc in node_spec.processors:
82+
proc_spec = spec.processors[proc.name]
83+
ssh_credentials = keystore.ssh_credentials.get(proc.ssh_credentials)
84+
github_credentials = keystore.github_credentials.get(proc_spec.source)
85+
86+
dor_address = extract_address(spec.nodes[proc_spec.dor].rest_address) if proc_spec.dor else node_address
87+
dor = DORProxy(dor_address)
88+
89+
# fetch proc descriptor
90+
proc_descriptor = fetch_proc_descriptor(proc_spec.source, proc_spec.commit_id, proc_spec.proc_path,
91+
proc_spec.proc_config, github_credentials)
92+
# determine the content hash for the GPP
93+
c_hash = _generate_gpp_hash(proc_spec.source, proc_spec.commit_id, proc_spec.proc_path,
94+
proc_spec.proc_config, proc_descriptor.dict())
95+
# check if gpp already found in dor
96+
items = dor.search(c_hashes=[c_hash])
97+
if items:
98+
# use the first item if multiple exist
99+
obj_id = items[0].obj_id
100+
else:
101+
print(f"Uploading proc gpp to {dor_address}: {proc_spec}")
102+
meta = dor.add_gpp_data_object(proc_spec.source,
103+
proc_spec.commit_id,
104+
proc_spec.proc_path,
105+
proc_spec.proc_config,
106+
keystore.identity,
107+
github_credentials=github_credentials)
108+
obj_id = meta.obj_id
109+
110+
print(f"Deploying proc ({obj_id}): {proc}")
111+
rti.deploy(obj_id,
112+
keystore,
113+
proc.deployment,
114+
ssh_credentials=ssh_credentials,
115+
github_credentials=github_credentials)
116+
117+
118+
class Compose(CLICommand):
119+
def __init__(self) -> None:
120+
super().__init__('compose', 'deploy processors based on yml file', arguments=[
121+
Argument('file', metavar='file', type=str, nargs=1,
122+
help="file containing the content of the data object")
123+
])
124+
125+
def execute(self, args: dict) -> None:
126+
spec_file = args["file"][0]
127+
with open(spec_file, 'r') as stream:
128+
data_loaded = yaml.load(stream, Loader=yaml.Loader)
129+
130+
spec: DeploySpec = DeploySpec.parse_obj(data_loaded)
131+
132+
nodes = dict()
133+
try:
134+
for node_key, node_spec in spec.nodes.items():
135+
print(f"Starting up {node_key} | {node_spec.keystore_id}")
136+
node = startup_node(node_spec)
137+
nodes[node_key] = node
138+
139+
print("Deploying processors to nodes")
140+
deploy_processors(spec, nodes)
141+
142+
print("Nodes ready")
143+
144+
# Block until interrupt
145+
signal_listener = SignalListener([signal.SIGTERM])
146+
while not signal_listener.triggered:
147+
time.sleep(1)
148+
except KeyboardInterrupt:
149+
print("Interrupted by user. Shutting down.")
150+
finally:
151+
for _, node in nodes.items():
152+
node.shutdown()
153+
154+
155+
if __name__ == '__main__':
156+
compose = Compose()
157+
compose.execute({
158+
"file": ["/Users/reynoldmok/Library/Application Support/JetBrains/PyCharm2022.2/scratches/compose.yml"],
159+
})

0 commit comments

Comments
 (0)