Sardou 0.1.0__tar.gz
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.
- sardou-0.1.0/PKG-INFO +6 -0
- sardou-0.1.0/README.md +91 -0
- sardou-0.1.0/Sardou.egg-info/PKG-INFO +6 -0
- sardou-0.1.0/Sardou.egg-info/SOURCES.txt +11 -0
- sardou-0.1.0/Sardou.egg-info/dependency_links.txt +1 -0
- sardou-0.1.0/Sardou.egg-info/requires.txt +1 -0
- sardou-0.1.0/Sardou.egg-info/top_level.txt +1 -0
- sardou-0.1.0/pyproject.toml +17 -0
- sardou-0.1.0/sardou/__init__.py +2 -0
- sardou-0.1.0/sardou/requirements.py +75 -0
- sardou-0.1.0/sardou/sardou.py +60 -0
- sardou-0.1.0/sardou/validation.py +68 -0
- sardou-0.1.0/setup.cfg +4 -0
sardou-0.1.0/PKG-INFO
ADDED
sardou-0.1.0/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# TOSCA in Swarmchestrate
|
|
2
|
+
|
|
3
|
+
This repository is home to TOSCA in the [Swarmchestrate](https://www.swarmchestrate.eu/) project, which will use TOSCA v2.0 to describe applications and capacities managed in a Swarmchestrate Universe.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Sardou TOSCA Library
|
|
7
|
+
|
|
8
|
+
Sardou validates and extracts info from a Swarmchestrate TOSCA template.
|
|
9
|
+
|
|
10
|
+
### Prerequisites
|
|
11
|
+
- Python 3.10+
|
|
12
|
+
- [Puccini](https://github.com/tliron/puccini) 0.22.x
|
|
13
|
+
- Minimum GLIBC 2.34 (Ubuntu 22.04 or higher)
|
|
14
|
+
|
|
15
|
+
Install Puccini on Linux by:
|
|
16
|
+
```sh
|
|
17
|
+
wget https://github.com/tliron/puccini/releases/download/v0.22.7/puccini_0.22.7_linux_amd64.deb
|
|
18
|
+
sudo dpkg -i puccini_0.22.7_linux_amd64.deb || sudo apt --fix-broken install -y
|
|
19
|
+
rm -f puccini_0.22.7_linux_amd64.deb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
23
|
+
|
|
24
|
+
Install using `pip` pointed at this GitHub repo. PyPI package coming soon.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install git+https://github.com/Swarmchestrate/tosca.git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Usage
|
|
31
|
+
|
|
32
|
+
Import the Sardou TOSCA Library
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from sardou import Sardou # note the uppercase F
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create a new `Sardou` object, passing it the path to your Swarmchestrate TOSCA template.
|
|
39
|
+
This will validate the template. If there are errors or warnings, they will be presented here.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
>>> tosca = Sardou("my_app.yaml")
|
|
43
|
+
Processed successfully: my_app.yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Grab the QoS requirements as a Python object.
|
|
47
|
+
You could wrap this as a dictionary and dump to JSON or YAML.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
>>> tosca.get_qos()
|
|
51
|
+
[{'energy': {'type': 'swch:QoS.Energy.Budget', 'properties': {'priority': 0.3, 'target': 10}}}...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Grab the Resource requirements as a Python object.
|
|
55
|
+
You could dump this to JSON or YAML.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
>>> tosca.get_requirements()
|
|
59
|
+
{'worker-node': {'metadata': {'created_by': 'floria-tosca-lib', 'created_at': '2025-09-16T14:51:24Z', 'description': 'Generated from node worker-node', 'version': '1.0'}, 'capabilities': {'host': {'properties': {'num-cpus': {'$greater_than': 4}, 'mem-size': {'$greater_than': '8 GB'}}}, ...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can traverse YAML maps using dot notation if needed.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
>>> tosca.service_template.node_templates
|
|
66
|
+
{'swarm': {'type': 'swch:Swarm', 'directives': ['substitute']}, ...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Devs
|
|
70
|
+
|
|
71
|
+
It is recommended that developers open a GitHub Codespace on this repository, which includes dependencies and a Makefile for running Puccini manually.
|
|
72
|
+
|
|
73
|
+
## TOSCA Template Validation with Puccini
|
|
74
|
+
|
|
75
|
+
This is an added feature that provides a Python validation library and script to check whether TOSCA service templates are valid using the [Puccini](https://github.com/tliron/puccini) parser.
|
|
76
|
+
|
|
77
|
+
##### Validation Library (`lib/validation.py `)
|
|
78
|
+
- A library that defines the `validate_template()` function to validate a single TOSCA YAML file.
|
|
79
|
+
- Returns `True` if the template is valid, `False` if not.
|
|
80
|
+
|
|
81
|
+
##### Validation Script (`run_validation.py`)
|
|
82
|
+
- A script that searches the `templates/` folder and validates all `.yaml` files in one run.
|
|
83
|
+
- Prints total successes/failures and exits with code `1` if any file fails.
|
|
84
|
+
|
|
85
|
+
Run:
|
|
86
|
+
- `python3 run_validation.py`
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Contact
|
|
90
|
+
|
|
91
|
+
Contact Jay at Westminster for support with TOSCA and/or this repository.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
Sardou.egg-info/PKG-INFO
|
|
4
|
+
Sardou.egg-info/SOURCES.txt
|
|
5
|
+
Sardou.egg-info/dependency_links.txt
|
|
6
|
+
Sardou.egg-info/requires.txt
|
|
7
|
+
Sardou.egg-info/top_level.txt
|
|
8
|
+
sardou/__init__.py
|
|
9
|
+
sardou/requirements.py
|
|
10
|
+
sardou/sardou.py
|
|
11
|
+
sardou/validation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruamel.yaml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sardou
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "Sardou"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Sardou TOSCA Library"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name="Jay DesLauriers", email="j.deslauriers@westminster.ac.uk" }
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"ruamel.yaml"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["setuptools>=61.0"]
|
|
14
|
+
build-backend = "setuptools.build_meta"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
packages = ["sardou"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TOSCA Resource Requirements Extractor
|
|
3
|
+
Converts TOSCA node_filter data to ask.yaml format
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
def convert_node_filter_to_capabilities(node_filter):
|
|
9
|
+
"""Convert TOSCA node_filter to capabilities format"""
|
|
10
|
+
capabilities = {}
|
|
11
|
+
|
|
12
|
+
# Extract conditions from $and
|
|
13
|
+
conditions = node_filter.get('$and', [])
|
|
14
|
+
|
|
15
|
+
for condition in conditions:
|
|
16
|
+
for constraint_func, args in condition.items():
|
|
17
|
+
if len(args) >= 2:
|
|
18
|
+
# Parse the property path: [SELF, TARGET, CAPABILITY, capability_type, property_name]
|
|
19
|
+
property_path = args[0].get('$get_property', [])
|
|
20
|
+
if len(property_path) >= 5:
|
|
21
|
+
capability_type = property_path[3]
|
|
22
|
+
property_name = property_path[4]
|
|
23
|
+
constraint_value = args[1]
|
|
24
|
+
|
|
25
|
+
# Initialize capability structure
|
|
26
|
+
if capability_type not in capabilities:
|
|
27
|
+
capabilities[capability_type] = {'properties': {}}
|
|
28
|
+
|
|
29
|
+
# Set the constraint
|
|
30
|
+
if constraint_func == '$equal':
|
|
31
|
+
capabilities[capability_type]['properties'][property_name] = constraint_value
|
|
32
|
+
else:
|
|
33
|
+
capabilities[capability_type]['properties'][property_name] = {constraint_func: constraint_value}
|
|
34
|
+
|
|
35
|
+
return capabilities
|
|
36
|
+
|
|
37
|
+
def extract_nodes_with_filter(tosca_dict):
|
|
38
|
+
"""Extract nodes that have node_filter from node_templates"""
|
|
39
|
+
nodes_with_filter = {}
|
|
40
|
+
|
|
41
|
+
node_templates = tosca_dict.get('service_template', {}).get('node_templates', {})
|
|
42
|
+
|
|
43
|
+
for node_name, node_data in node_templates.items():
|
|
44
|
+
if 'node_filter' in node_data:
|
|
45
|
+
nodes_with_filter[node_name] = node_data['node_filter']
|
|
46
|
+
|
|
47
|
+
return nodes_with_filter
|
|
48
|
+
|
|
49
|
+
def tosca_to_ask_dict(tosca_dict):
|
|
50
|
+
"""
|
|
51
|
+
Convert TOSCA dict to ask format dict
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
tosca_dict (dict): Parsed TOSCA YAML data
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
dict: Ask format data
|
|
58
|
+
"""
|
|
59
|
+
nodes_with_filter = extract_nodes_with_filter(tosca_dict)
|
|
60
|
+
ask_data = {}
|
|
61
|
+
|
|
62
|
+
for node_name, node_filter in nodes_with_filter.items():
|
|
63
|
+
ask_entry = {
|
|
64
|
+
'metadata': {
|
|
65
|
+
'created_by': 'sardou-tosca-lib',
|
|
66
|
+
'created_at': datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
67
|
+
'description': f'Generated from node {node_name}',
|
|
68
|
+
'version': '1.0'
|
|
69
|
+
},
|
|
70
|
+
'capabilities': convert_node_filter_to_capabilities(node_filter)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ask_data[node_name] = ask_entry
|
|
74
|
+
|
|
75
|
+
return ask_data
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
from ruamel.yaml import YAML
|
|
4
|
+
|
|
5
|
+
from .validation import validate_template
|
|
6
|
+
from .requirements import tosca_to_ask_dict
|
|
7
|
+
|
|
8
|
+
class DotDict:
|
|
9
|
+
def __init__(self, **entries):
|
|
10
|
+
for k, v in entries.items():
|
|
11
|
+
if isinstance(v, dict):
|
|
12
|
+
v = DotDict(**v)
|
|
13
|
+
elif isinstance(v, list):
|
|
14
|
+
v = [DotDict(**i) if isinstance(i, dict) else i for i in v]
|
|
15
|
+
setattr(self, k, v)
|
|
16
|
+
def __getitem__(self, key):
|
|
17
|
+
return getattr(self, key)
|
|
18
|
+
def __setitem__(self, key, value):
|
|
19
|
+
setattr(self, key, value)
|
|
20
|
+
def __delitem__(self, key):
|
|
21
|
+
delattr(self, key)
|
|
22
|
+
def __contains__(self, key):
|
|
23
|
+
return hasattr(self, key)
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return repr(self._to_dict())
|
|
26
|
+
def _to_dict(self):
|
|
27
|
+
result = {}
|
|
28
|
+
for key, value in self.__dict__.items():
|
|
29
|
+
if isinstance(value, DotDict):
|
|
30
|
+
result[key] = value._to_dict()
|
|
31
|
+
elif isinstance(value, list):
|
|
32
|
+
result[key] = [
|
|
33
|
+
v._to_dict() if isinstance(v, DotDict) else v for v in value
|
|
34
|
+
]
|
|
35
|
+
else:
|
|
36
|
+
result[key] = value
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
def _to_json(self, indent=None, **kwargs):
|
|
40
|
+
return json.dumps(self._to_dict(), indent=indent, **kwargs)
|
|
41
|
+
|
|
42
|
+
class Sardou(DotDict):
|
|
43
|
+
def __init__(self, path):
|
|
44
|
+
path = Path(path)
|
|
45
|
+
if not path.exists():
|
|
46
|
+
raise FileNotFoundError(f"File does not exist: {path}")
|
|
47
|
+
if not validate_template(path):
|
|
48
|
+
raise ValueError(f"Validation failed for: {path}")
|
|
49
|
+
yaml = YAML(typ='safe')
|
|
50
|
+
with path.open('r') as f:
|
|
51
|
+
data = yaml.load(f)
|
|
52
|
+
super().__init__(**data)
|
|
53
|
+
|
|
54
|
+
def get_requirements(self):
|
|
55
|
+
return tosca_to_ask_dict(self._to_dict())
|
|
56
|
+
|
|
57
|
+
def get_qos(self, indent=None, **kwargs):
|
|
58
|
+
policies = self.service_template.policies
|
|
59
|
+
return [p._to_dict() if isinstance(p, DotDict) else p for p in policies]
|
|
60
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
from ruamel.yaml import YAML
|
|
6
|
+
|
|
7
|
+
PUCCINI_CMD = "/usr/bin/puccini-tosca"
|
|
8
|
+
PUCCINI_FLAGS = ["-x", "data_types.string.permissive"]
|
|
9
|
+
|
|
10
|
+
# Read and update YAML using ruamel.yaml
|
|
11
|
+
yaml = YAML()
|
|
12
|
+
|
|
13
|
+
def prevalidate(file_path: Path) -> bool:
|
|
14
|
+
# Check if file exists
|
|
15
|
+
if not file_path.exists():
|
|
16
|
+
print(f"File does not exist: {file_path}")
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
with file_path.open('r') as f:
|
|
21
|
+
data = yaml.load(f)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"Error reading YAML file {file_path}: {e}")
|
|
24
|
+
return False
|
|
25
|
+
if not data:
|
|
26
|
+
print(f"No YAML content found")
|
|
27
|
+
return False
|
|
28
|
+
imports = data.get('imports', [])
|
|
29
|
+
nodes = data["service_template"].get('node_templates', {})
|
|
30
|
+
|
|
31
|
+
for imp in imports:
|
|
32
|
+
if isinstance(imp, dict) and 'profile' in imp:
|
|
33
|
+
imp['url'] = imp.pop('profile')
|
|
34
|
+
|
|
35
|
+
for _, node in nodes.items():
|
|
36
|
+
node.pop('node_filter', None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_template(file_path: Path) -> bool:
|
|
43
|
+
# will run the puccini-tosca parse <with flag>
|
|
44
|
+
yaml_data = prevalidate(file_path)
|
|
45
|
+
|
|
46
|
+
# open a temp file
|
|
47
|
+
with NamedTemporaryFile() as temp_file:
|
|
48
|
+
yaml.dump(yaml_data, temp_file)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
[PUCCINI_CMD, "parse", str(temp_file.name)] + PUCCINI_FLAGS,
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True
|
|
55
|
+
)
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
print(f"Processed successfully: {file_path} \n")
|
|
58
|
+
return True
|
|
59
|
+
else:
|
|
60
|
+
print(f"Failed to process: {file_path} \n")
|
|
61
|
+
print("==== Error Output ====")
|
|
62
|
+
print(result.stderr.strip() or result.stdout.strip())
|
|
63
|
+
print("======================")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
print(f"Puccini not found at {PUCCINI_CMD}. Please install it first.")
|
|
68
|
+
sys.exit(1)
|
sardou-0.1.0/setup.cfg
ADDED