Skip to content

Commit

Permalink
Merge branch 'eclipse-basyx:main' into add_compliance-tool
Browse files Browse the repository at this point in the history
  • Loading branch information
Frosty2500 authored Nov 28, 2024
2 parents 9ce9861 + c360897 commit 103f14d
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ sdk/test/adapter/schemas

# Ignore dynamically generated version file
sdk/basyx/version.py

# ignore the content of the server storage
server/storage/
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ Python's builtin `unittest`. To install the required tools, use:
pip install .[dev]
```

> [!note]
> The `.` denotes the current directory and needs to be the directory the `pyproject.toml` is located in.
> Therefore, you need to run this command and the ones below in the `/sdk` directory (relative to the repository root).
Running all checks:
```bash
mypy basyx test
Expand Down
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ The Eclipse BaSyx Python project focuses on providing a Python implementation of
for Industry 4.0 Systems.
These are the currently implemented specifications:

| Specification | Version |
|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Part 1: Metamodel | [v3.0 (01001-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/06/IDTA-01001-3-0_SpecificationAssetAdministrationShell_Part1_Metamodel.pdf) |
| Part 2: API | [v3.0 (01002-3-0)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2023/06/IDTA-01002-3-0_SpecificationAssetAdministrationShell_Part2_API_.pdf) |
| Specification | Version |
|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Part 1: Metamodel | [v3.0.1 (01001-3-1)](https://industrialdigitaltwin.org/wp-content/uploads/2024/06/IDTA-01001-3-0-1_SpecificationAssetAdministrationShell_Part1_Metamodel.pdf) |
| Schemata (JSONSchema, XSD) | [v3.0.8 (IDTA-01001-3-0-1_schemasV3.0.8)](https://github.com/admin-shell-io/aas-specs/releases/tag/IDTA-01001-3-0-1_schemasV3.0.8) |
| Part 2: API | [v3.0 (01002-3-0)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2023/06/IDTA-01002-3-0_SpecificationAssetAdministrationShell_Part2_API_.pdf) |
| Part 3a: Data Specification IEC 61360 | [v3.0 (01003-a-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01003-a-3-0_SpecificationAssetAdministrationShell_Part3a_DataSpecification_IEC61360.pdf) |
| Part 5: Package File Format (AASX) | [v3.0 (01005-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01005-3-0_SpecificationAssetAdministrationShell_Part5_AASXPackageFileFormat.pdf) |
| Part 5: Package File Format (AASX) | [v3.0 (01005-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01005-3-0_SpecificationAssetAdministrationShell_Part5_AASXPackageFileFormat.pdf) |

## Features
This repository is structured into separate packages.
The `sdk` directory provides the AAS metamodel as Python objects and fundamental functionalities to handle AAS.
The `server` is to be determined.
The `server` implements a specification-compliant Docker HTTP server for AASs.
The `compliance_tool` is to be determined.

* SDK
* [SDK](./sdk/README.md):
* Modelling of AASs as Python objects
* **except for**: *HasDataSpecification*
* Reading and writing of AASX package files
* (De-)serialization of AAS objects into/from JSON and XML
* Storing of AAS objects in CouchDB, Backend infrastructure for easy expansion
* Compliance checking of AAS XML and JSON files
* Server (tbd)
* [Server](./server/README.md): Docker Image of a specification compliant HTTP Server implementing the interfaces:
* Asset Administration Shell Repository
* Submodel Repository
* Compliance Tool (tbd)

## License
Expand All @@ -49,7 +51,7 @@ Additionally, security fixes may be released at any point.

## Contributing

For contributing with issues and code, please see our [Contribution Guideline](../CONTRIBUTING.md).
For contributing with issues and code, please see our [Contribution Guideline](./CONTRIBUTING.md).

### Eclipse Contributor Agreement

Expand Down
2 changes: 1 addition & 1 deletion sdk/basyx/aas/model/_string_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def check_name_type(value: str, type_name: str = "NameType") -> None:


def check_path_type(value: str, type_name: str = "PathType") -> None:
return check_identifier(value, type_name)
return check(value, type_name, 1, 2000)


def check_qualifier_type(value: str, type_name: str = "QualifierType") -> None:
Expand Down
4 changes: 4 additions & 0 deletions sdk/basyx/aas/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,10 @@ def __init__(self) -> None:
def kind(self):
return self._kind

@kind.setter
def kind(self, value: ModellingKind):
self._kind = value


class Qualifiable(Namespace, metaclass=abc.ABCMeta):
"""
Expand Down
45 changes: 45 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
FROM python:3.11-alpine

LABEL org.label-schema.name="Eclipse BaSyx" \
org.label-schema.version="1.0" \
org.label-schema.description="Docker image for the basyx-python-sdk server application" \
org.label-schema.maintainer="Eclipse BaSyx"

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# If we have more dependencies for the server it would make sense
# to refactor uswgi to the pyproject.toml
RUN apk update && \
apk add --no-cache nginx supervisor gcc musl-dev linux-headers python3-dev git bash && \
pip install uwsgi && \
pip install --no-cache-dir git+https://github.com/eclipse-basyx/basyx-python-sdk@main#subdirectory=sdk && \
apk del git bash


COPY uwsgi.ini /etc/uwsgi/
COPY supervisord.ini /etc/supervisor/conf.d/supervisord.ini
COPY stop-supervisor.sh /etc/supervisor/stop-supervisor.sh
RUN chmod +x /etc/supervisor/stop-supervisor.sh

# Makes it possible to use a different configuration
ENV UWSGI_INI=/etc/uwsgi/uwsgi.ini
# object stores aren't thread-safe yet
# https://github.com/eclipse-basyx/basyx-python-sdk/issues/205
ENV UWSGI_CHEAPER=0
ENV UWSGI_PROCESSES=1
ENV NGINX_MAX_UPLOAD=1M
ENV NGINX_WORKER_PROCESSES=1
ENV LISTEN_PORT=80
ENV CLIENT_BODY_BUFFER_SIZE=1M

# Copy the entrypoint that will generate Nginx additional configs
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

COPY ./app /app
WORKDIR /app

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.ini"]
97 changes: 97 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Eclipse BaSyx Python SDK - HTTP Server

This package contains a Dockerfile to spin up an exemplary HTTP/REST server following the [Specification of the AAS Part 2 API][6] with ease.
The server currently implements the following interfaces:

- [Asset Administration Shell Repository Service][4]
- [Submodel Repository Service][5]

It uses the [HTTP API][1] and the [AASX][7], [JSON][8], and [XML][9] Adapters of the [BaSyx Python SDK][3], to serve regarding files from a given directory.
The files are only read, chages won't persist.

Alternatively, the container can also be told to use the [Local-File Backend][2] instead, which stores AAS and Submodels as individual JSON files and allows for persistent changes (except supplementary files, i.e. files referenced by `File` submodel elements).
See [below](#options) on how to configure this.

## Building
The container image can be built via:
```
$ docker buildx build -t basyx-python-sdk-http-server .
```

## Running

### Storage
The container needs to be provided with the directory `/storage` to store AAS and Submodel files: AASX, JSON, XML or JSON files of Local-File Backend.

This directory can be mapped via the `-v` option from another image or a local directory.
To map the directory `storage` inside the container, `-v ./storage:/storage` can be used.
The directory `storage` will be created in the current working directory, if it doesn't already exist.

### Port
The HTTP server inside the container listens on port 80 by default.
To expose it on the host on port 8080, use the option `-p 8080:80` when running it.

### Options
The container can be configured via environment variables:
- `API_BASE_PATH` determines the base path under which all other API paths are made available.
Default: `/api/v3.0`
- `STORAGE_TYPE` can be one of `LOCAL_FILE_READ_ONLY` or `LOCAL_FILE_BACKEND`:
- When set to `LOCAL_FILE_READ_ONLY` (the default), the server will read and serve AASX, JSON, XML files from the storage directory.
The files are not modified, all changes done via the API are only stored in memory.
- When instead set to `LOCAL_FILE`, the server makes use of the [LocalFileBackend][2], where AAS and Submodels are persistently stored as JSON files.
Supplementary files, i.e. files referenced by `File` submodel elements, are not stored in this case.
- `STORAGE_PATH` sets the directory to read the files from *within the container*. If you bind your files to a directory different from the default `/storage`, you can use this variable to adjust the server accordingly.

### Running Examples

Putting it all together, the container can be started via the following command:
```
$ docker run -p 8080:80 -v ./storage:/storage basyx-python-sdk-http-server
```

Since Windows uses backslashes instead of forward slashes in paths, you'll have to adjust the path to the storage directory there:
```
> docker run -p 8080:80 -v .\storage:/storage basyx-python-sdk-http-server
```

Per default, the server will use the `LOCAL_FILE_READ_ONLY` storage type and serve the API under `/api/v3.0` and read files from `/storage`. If you want to change this, you can do so like this:
```
$ docker run -p 8080:80 -v ./storage2:/storage2 -e API_BASE_PATH=/api/v3.1 -e STORAGE_TYPE=LOCAL_FILE_BACKEND -e STORAGE_PATH=/storage2 basyx-python-sdk-http-server
```

## Building and running the image with docker-compose

The container image can also be built and run via:
```
$ docker compose up
```

This is the exemplary `docker-compose` file for the server:
````yaml
services:
app:
build: .
ports:
- "8080:80"
volumes:
- ./storage:/storage

````

Here files are read from `/storage` and the server can be accessed at http://localhost:8080/api/v3.0/ from your host system.
To get a different setup this compose.yaml file can be adapted and expanded.

## Acknowledgments

This Dockerfile is inspired by the [tiangolo/uwsgi-nginx-docker][10] repository.

[1]: https://github.com/eclipse-basyx/basyx-python-sdk/pull/238
[2]: https://basyx-python-sdk.readthedocs.io/en/latest/backend/local_file.html
[3]: https://github.com/eclipse-basyx/basyx-python-sdk
[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.0.1_SSP-001
[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.0.1_SSP-001
[6]: https://industrialdigitaltwin.org/content-hub/aasspecifications/idta_01002-3-0_application_programming_interfaces
[7]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/aasx.html#adapter-aasx
[8]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/json.html
[9]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/xml.html
[10]: https://github.com/tiangolo/uwsgi-nginx-docker
46 changes: 46 additions & 0 deletions server/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
import pathlib
import sys

from basyx.aas import model, adapter
from basyx.aas.adapter import aasx

from basyx.aas.backend.local_file import LocalFileObjectStore
from basyx.aas.adapter.http import WSGIApp

storage_path = os.getenv("STORAGE_PATH", "/storage")
storage_type = os.getenv("STORAGE_TYPE", "LOCAL_FILE_READ_ONLY")
base_path = os.getenv("API_BASE_PATH")

wsgi_optparams = {}

if base_path is not None:
wsgi_optparams["base_path"] = base_path

if storage_type == "LOCAL_FILE_BACKEND":
application = WSGIApp(LocalFileObjectStore(storage_path), aasx.DictSupplementaryFileContainer(), **wsgi_optparams)

elif storage_type in "LOCAL_FILE_READ_ONLY":
object_store: model.DictObjectStore = model.DictObjectStore()
file_store: aasx.DictSupplementaryFileContainer = aasx.DictSupplementaryFileContainer()

for file in pathlib.Path(storage_path).iterdir():
if not file.is_file():
continue
print(f"Loading {file}")

if file.suffix.lower() == ".json":
with open(file) as f:
adapter.json.read_aas_json_file_into(object_store, f)
elif file.suffix.lower() == ".xml":
with open(file) as f:
adapter.xml.read_aas_xml_file_into(object_store, file)
elif file.suffix.lower() == ".aasx":
with aasx.AASXReader(file) as reader:
reader.read_into(object_store=object_store, file_store=file_store)

application = WSGIApp(object_store, file_store, **wsgi_optparams)

else:
print(f"STORAGE_TYPE must be either LOCAL_FILE or LOCAL_FILE_READ_ONLY! Current value: {storage_type}",
file=sys.stderr)
7 changes: 7 additions & 0 deletions server/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
app:
build: .
ports:
- "8080:80"
volumes:
- ./storage:/storage
71 changes: 71 additions & 0 deletions server/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env sh
set -e

# Get the maximum upload file size for Nginx, default to 0: unlimited
USE_NGINX_MAX_UPLOAD=${NGINX_MAX_UPLOAD:-0}

# Get the number of workers for Nginx, default to 1
USE_NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1}

# Set the max number of connections per worker for Nginx, if requested
# Cannot exceed worker_rlimit_nofile, see NGINX_WORKER_OPEN_FILES below
NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-1024}

# Get the listen port for Nginx, default to 80
USE_LISTEN_PORT=${LISTEN_PORT:-80}

# Get the client_body_buffer_size for Nginx, default to 1M
USE_CLIENT_BODY_BUFFER_SIZE=${CLIENT_BODY_BUFFER_SIZE:-1M}

# Create the conf.d directory if it doesn't exist
if [ ! -d /etc/nginx/conf.d ]; then
mkdir -p /etc/nginx/conf.d
fi

if [ -f /app/nginx.conf ]; then
cp /app/nginx.conf /etc/nginx/nginx.conf
else
content='user nginx;\n'
# Set the number of worker processes in Nginx
content=$content"worker_processes ${USE_NGINX_WORKER_PROCESSES};\n"
content=$content'error_log /var/log/nginx/error.log warn;\n'
content=$content'pid /var/run/nginx.pid;\n'
content=$content'events {\n'
content=$content" worker_connections ${NGINX_WORKER_CONNECTIONS};\n"
content=$content'}\n'
content=$content'http {\n'
content=$content' include /etc/nginx/mime.types;\n'
content=$content' default_type application/octet-stream;\n'
content=$content' log_format main '"'\$remote_addr - \$remote_user [\$time_local] \"\$request\" '\n"
content=$content' '"'\$status \$body_bytes_sent \"\$http_referer\" '\n"
content=$content' '"'\"\$http_user_agent\" \"\$http_x_forwarded_for\"';\n"
content=$content' access_log /var/log/nginx/access.log main;\n'
content=$content' sendfile on;\n'
content=$content' keepalive_timeout 65;\n'
content=$content' include /etc/nginx/conf.d/*.conf;\n'
content=$content'}\n'
content=$content'daemon off;\n'
# Set the max number of open file descriptors for Nginx workers, if requested
if [ -n "${NGINX_WORKER_OPEN_FILES}" ] ; then
content=$content"worker_rlimit_nofile ${NGINX_WORKER_OPEN_FILES};\n"
fi
# Save generated /etc/nginx/nginx.conf
printf "$content" > /etc/nginx/nginx.conf

content_server='server {\n'
content_server=$content_server" listen ${USE_LISTEN_PORT};\n"
content_server=$content_server' location / {\n'
content_server=$content_server' include uwsgi_params;\n'
content_server=$content_server' uwsgi_pass unix:///tmp/uwsgi.sock;\n'
content_server=$content_server' }\n'
content_server=$content_server'}\n'
# Save generated server /etc/nginx/conf.d/nginx.conf
printf "$content_server" > /etc/nginx/conf.d/nginx.conf

# # Generate additional configuration
printf "client_max_body_size $USE_NGINX_MAX_UPLOAD;\n" > /etc/nginx/conf.d/upload.conf
printf "client_body_buffer_size $USE_CLIENT_BODY_BUFFER_SIZE;\n" > /etc/nginx/conf.d/body-buffer-size.conf
printf "add_header Access-Control-Allow-Origin *;\n" > /etc/nginx/conf.d/cors-header.conf
fi

exec "$@"
8 changes: 8 additions & 0 deletions server/stop-supervisor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh

printf "READY\n"

while read line; do
echo "Processing Event: $line" >&2
kill $PPID
done < /dev/stdin
27 changes: 27 additions & 0 deletions server/supervisord.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[supervisord]
nodaemon=true

[program:uwsgi]
command=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs = 0
autorestart=false
# may make sense to have autorestart enabled in production

[program:nginx]
command=/usr/sbin/nginx
stdout_logfile=/var/log/nginx.out.log
stdout_logfile_maxbytes=0
stderr_logfile=/var/log/nginx.err.log
stderr_logfile_maxbytes=0
stopsignal=QUIT
startsecs = 0
autorestart=false
# may make sense to have autorestart enabled in production

[eventlistener:quit_on_failure]
events=PROCESS_STATE_STOPPED,PROCESS_STATE_EXITED,PROCESS_STATE_FATAL
command=/etc/supervisor/stop-supervisor.sh
Loading

0 comments on commit 103f14d

Please sign in to comment.