Initial import
Depends-On: https://review.opendev.org/c/openstack/governance/+/910581 Change-Id: I87866224d9ac89d3da150c95a86a20afb48dc4d2
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
.stestr/
|
||||
|
||||
wrk/
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
default_language_version:
|
||||
# force all unspecified python hooks to run python3
|
||||
python: python3
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: mixed-line-ending
|
||||
args: ['--fix', 'lf']
|
||||
exclude: '.*\.(svg)$'
|
||||
- id: check-byte-order-marker
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: check-yaml
|
||||
files: .*\.(yaml|yml)$
|
||||
exclude: '^zuul.d/.*$'
|
||||
- repo: https://github.com/PyCQA/doc8
|
||||
rev: v1.1.1
|
||||
hooks:
|
||||
- id: doc8
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.2.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['-l', '79']
|
||||
exclude: '^codegenerator/templates/.*$'
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
language: python
|
||||
types: [python]
|
||||
args: ["codegenerator"]
|
||||
pass_filenames: false
|
||||
additional_dependencies:
|
||||
- types-decorator
|
||||
- types-PyYAML
|
||||
@@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./codegenerator/tests/unit
|
||||
top_dir=./
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
=======================
|
||||
OpenStack CodeGenerator
|
||||
=======================
|
||||
|
||||
Primary goal of the project is to simplify maintainers life by generating
|
||||
complete or at least parts of the code.
|
||||
|
||||
OpenApi Schema
|
||||
==============
|
||||
|
||||
CodeGenerator is able to generate OpenAPI specs for certain services by
|
||||
inspecting their code. This requires service package being installed in the
|
||||
environment where the generator is running. It then tries to initialize service
|
||||
application and for supported runtimes scans for the exposed operations. At the
|
||||
moment following services are covered:
|
||||
|
||||
- Nova
|
||||
|
||||
- Neutron
|
||||
|
||||
- Cinder
|
||||
|
||||
- Glance
|
||||
|
||||
- Keystone
|
||||
|
||||
- Octavia
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# This is a cross-platform list tracking distribution packages needed for
|
||||
# install and tests;
|
||||
# see https://docs.openstack.org/infra/bindep/ for additional information.
|
||||
|
||||
build-essential [platform:dpkg test]
|
||||
gcc [platform:rpm test]
|
||||
|
||||
# Cinder/Keystone/... require ssl and crypt to be present
|
||||
cryptsetup [platform:rpm]
|
||||
cryptsetup-bin [platform:dpkg]
|
||||
libssl-dev [platform:dpkg]
|
||||
openssl-devel [platform:rpm]
|
||||
@@ -0,0 +1,101 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from codegenerator.base import BaseGenerator
|
||||
|
||||
from openstack.test.fakes import generate_fake_resource
|
||||
|
||||
|
||||
class AnsibleGenerator(BaseGenerator):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def _render_command(
|
||||
self,
|
||||
context: dict,
|
||||
osc_path: list,
|
||||
impl_template: str,
|
||||
impl_dest: Path,
|
||||
test_template: str,
|
||||
test_dest: Path,
|
||||
):
|
||||
"""Render command code"""
|
||||
self._render(impl_template, context, impl_dest.parent, impl_dest.name)
|
||||
|
||||
unittest_path = test_dest.parent
|
||||
|
||||
unittest_path.mkdir(parents=True, exist_ok=True)
|
||||
Path(unittest_path, "__init__.py").touch()
|
||||
|
||||
self._render(test_template, context, test_dest.parent, test_dest.name)
|
||||
|
||||
def generate(self, res, target_dir, args=None):
|
||||
"""Generate code for the Ansible"""
|
||||
logging.debug("Generating Ansible code in %s" % target_dir)
|
||||
ansible_path = ["plugins", "modules"]
|
||||
|
||||
context = dict(
|
||||
res=res.resource_class,
|
||||
sdk_mod_name=res.mod_name,
|
||||
class_name=res.class_name,
|
||||
resource_name=res.class_name.lower(),
|
||||
sdk_service_name=res.service_name,
|
||||
proxy=res.proxy_obj,
|
||||
fqcn=res.fqcn,
|
||||
registry_name=res.registry_name,
|
||||
attrs=res.attrs,
|
||||
target_name=res.class_name.lower(),
|
||||
)
|
||||
if args and args.alternative_target_name:
|
||||
context["target_name"] = args.alternative_target_name
|
||||
context["ansible_module_name"] = "".join(
|
||||
[x.capitalize() for x in context["target_name"].split("_")]
|
||||
)
|
||||
|
||||
work_dir = Path(target_dir, "ansible")
|
||||
|
||||
# Generate fake resource to use in examples and tests
|
||||
fake_resource = generate_fake_resource(res.resource_class)
|
||||
context["fake_resource"] = fake_resource
|
||||
|
||||
# Generate info module
|
||||
self._render(
|
||||
"ansible/impl_mod_info.py.j2",
|
||||
context,
|
||||
Path(work_dir, "/".join(ansible_path)),
|
||||
f"{context['target_name']}_info.py",
|
||||
)
|
||||
# Generate module
|
||||
self._render(
|
||||
"ansible/impl_mod.py.j2",
|
||||
context,
|
||||
Path(work_dir, "/".join(ansible_path)),
|
||||
f"{context['target_name']}.py",
|
||||
)
|
||||
# Generate ansible test role
|
||||
tests_dir = Path(work_dir, "ci/roles/", context["target_name"])
|
||||
self._render(
|
||||
"ansible/test_playbook.yaml.j2",
|
||||
context,
|
||||
Path(tests_dir, "ci/roles/", context["target_name"], "tasks"),
|
||||
"main.yaml",
|
||||
)
|
||||
# Format rendered code to have less flake complains. This will still
|
||||
# not guarantee code is fitting perfect, since there might be too long
|
||||
# lines
|
||||
self._format_code(
|
||||
Path(work_dir, "/".join(ansible_path)),
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import abc
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import mdformat as md
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from jinja2 import select_autoescape
|
||||
from jinja2 import StrictUndefined
|
||||
|
||||
|
||||
def wrap_markdown(input: str, width: int = 79) -> str:
|
||||
"""Apply mardownify to wrap the markdown"""
|
||||
return md.text(input, options={"wrap": width})
|
||||
|
||||
|
||||
class BaseGenerator:
|
||||
def __init__(self):
|
||||
# Lower debug level of mdformat
|
||||
logging.getLogger("markdown_it").setLevel(logging.INFO)
|
||||
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader("codegenerator/templates"),
|
||||
autoescape=select_autoescape(),
|
||||
undefined=StrictUndefined,
|
||||
)
|
||||
self.env.filters["wrap_markdown"] = wrap_markdown
|
||||
|
||||
def get_parser(self, parser):
|
||||
return parser
|
||||
|
||||
def _render(self, template, context, dest, fname):
|
||||
"""Render single template"""
|
||||
template = self.env.get_template(template)
|
||||
content = template.render(**context)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
with open(Path(dest, fname), "w") as fp:
|
||||
logging.debug("Writing %s" % (fp.name))
|
||||
fp.write(content)
|
||||
|
||||
def _format_code(self, *args):
|
||||
"""Format code using Black
|
||||
|
||||
:param *args: Path to the code to format
|
||||
"""
|
||||
for path in args:
|
||||
subprocess.run(["black", "-l", "79", path])
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate(
|
||||
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
||||
):
|
||||
pass
|
||||
|
||||
def generate_mod(
|
||||
self,
|
||||
target_dir,
|
||||
mod_path,
|
||||
mod_list: set[str],
|
||||
url: str,
|
||||
resouce_name: str,
|
||||
service_name: str,
|
||||
):
|
||||
pass
|
||||
@@ -0,0 +1,344 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from openstack import resource
|
||||
from sphinx import pycode
|
||||
import yaml
|
||||
|
||||
from codegenerator.ansible import AnsibleGenerator
|
||||
from codegenerator import common
|
||||
from codegenerator.jsonschema import JsonSchemaGenerator
|
||||
from codegenerator.metadata import MetadataGenerator
|
||||
from codegenerator.openapi_spec import OpenApiSchemaGenerator
|
||||
from codegenerator.osc import OSCGenerator
|
||||
from codegenerator.rust_cli import RustCliGenerator
|
||||
from codegenerator.rust_sdk import RustSdkGenerator
|
||||
from codegenerator.types import Metadata
|
||||
|
||||
|
||||
class ResourceProcessor:
|
||||
def __init__(self, mod_name, class_name):
|
||||
self.mod_name = mod_name
|
||||
self.class_name = class_name
|
||||
self.class_plural_name = (
|
||||
class_name + "s" if class_name[:-1] != "y" else "ies"
|
||||
)
|
||||
|
||||
spec = importlib.util.find_spec(self.mod_name)
|
||||
if not spec:
|
||||
raise RuntimeError("Module %s not found" % self.mod_name)
|
||||
self.module = importlib.util.module_from_spec(spec)
|
||||
if not self.module:
|
||||
raise RuntimeError("Error loading module %s" % self.mod_name)
|
||||
sys.modules[self.mod_name] = self.module
|
||||
if not spec.loader:
|
||||
raise RuntimeError("No module loader available")
|
||||
spec.loader.exec_module(self.module)
|
||||
self.resource_class = getattr(self.module, self.class_name)
|
||||
|
||||
# Get resource proxy
|
||||
srv_ver_mod, _, _ = self.mod_name.rpartition(".")
|
||||
proxy_mod_name = srv_ver_mod + "._proxy"
|
||||
proxy_spec = importlib.util.find_spec(proxy_mod_name)
|
||||
if not proxy_spec:
|
||||
raise RuntimeError("Module %s not found" % proxy_mod_name)
|
||||
self.proxy_mod = importlib.util.module_from_spec(proxy_spec)
|
||||
if not self.proxy_mod:
|
||||
raise RuntimeError("Error loading module %s" % proxy_mod_name)
|
||||
sys.modules[proxy_mod_name] = self.proxy_mod
|
||||
if not proxy_spec.loader:
|
||||
raise RuntimeError("No module loader available")
|
||||
proxy_spec.loader.exec_module(self.proxy_mod)
|
||||
self.proxy_obj = getattr(self.proxy_mod, "Proxy")
|
||||
self.srv_ver_mod = srv_ver_mod
|
||||
|
||||
self.service_name = self.mod_name.split(".")[1]
|
||||
self.fqcn = f"{self.mod_name}.{self.class_name}"
|
||||
|
||||
# Find the resource registry name
|
||||
for k, v in self.proxy_obj._resource_registry.items():
|
||||
if (
|
||||
hasattr(v, "__module__")
|
||||
and v.__module__ == self.mod_name
|
||||
and v.__name__ == self.class_name
|
||||
):
|
||||
self.registry_name = f"{self.service_name}.{k}"
|
||||
|
||||
self.attrs = dict()
|
||||
self.process()
|
||||
|
||||
def process(self):
|
||||
attr_docs = self.get_attr_docs()
|
||||
for k, v in self.body_attrs():
|
||||
doc = attr_docs.get(k)
|
||||
if doc:
|
||||
doc = re.sub("\\*Type: .*\\*", "", doc)
|
||||
doc = doc.rstrip()
|
||||
if not doc and k == "name":
|
||||
doc = "Name"
|
||||
elif not doc and k == "tags":
|
||||
doc = f"{self.class_name} Tags."
|
||||
self.attrs[k] = dict(attr=v, docs=doc)
|
||||
|
||||
def get_attr_docs(self):
|
||||
mod = pycode.ModuleAnalyzer.for_module(self.mod_name)
|
||||
mod.analyze()
|
||||
result = {}
|
||||
for k, v in mod.attr_docs.items():
|
||||
if k[0] == self.class_name:
|
||||
result[k[1]] = " ".join(v)
|
||||
if "id" not in result:
|
||||
result["id"] = "Id of the resource"
|
||||
return result
|
||||
|
||||
def body_attrs(self):
|
||||
for attr in inspect.getmembers(self.resource_class):
|
||||
if isinstance(attr[1], resource.Body):
|
||||
yield attr
|
||||
|
||||
|
||||
class Generator:
|
||||
schemas: dict = {}
|
||||
metadata: Metadata
|
||||
|
||||
def get_openapi_spec(self, path: Path):
|
||||
logging.debug("Fetch %s", path)
|
||||
if path.as_posix() not in self.schemas:
|
||||
self.schemas[path.as_posix()] = common.get_openapi_spec(
|
||||
path.as_posix()
|
||||
)
|
||||
return self.schemas[path.as_posix()]
|
||||
|
||||
def load_metadata(self, path: Path):
|
||||
with open(path, "r") as fp:
|
||||
data = yaml.safe_load(fp)
|
||||
self.metadata = Metadata(**data)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate code from OpenStackSDK resource definitions"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--module",
|
||||
# required=True,
|
||||
help="OpenStackSDK Module name (i.e. openstack.identity.v3.project)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--class-name",
|
||||
# required=True,
|
||||
help="OpenStackSDK Class name (under the specified module)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
required=True,
|
||||
choices=[
|
||||
"osc",
|
||||
"ansible",
|
||||
"rust-sdk",
|
||||
"rust-cli",
|
||||
"openapi-spec",
|
||||
"jsonschema",
|
||||
"metadata",
|
||||
],
|
||||
help="Target for which to generate code",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-dir", help="Working directory for the generated code"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alternative-module-path",
|
||||
help=("Optional new module path"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alternative-module-name",
|
||||
help=("Optional new module name " "(rename get into list)"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--openapi-yaml-spec",
|
||||
help=("Path to the OpenAPI spec file (yaml)"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--openapi-operation-id",
|
||||
help=("OpenAPI operationID"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--service-type",
|
||||
help=("Catalog service type"),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--api-version",
|
||||
help=("Api version (used in path for resulting code, i.e. v1)"),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--metadata",
|
||||
help=("Metadata file to load"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--service",
|
||||
help=("Metadata service name filter"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resource",
|
||||
help=("Metadata resource name filter"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--validate",
|
||||
action="store_true",
|
||||
help=("Metadata resource name filter"),
|
||||
)
|
||||
|
||||
generators = {
|
||||
"osc": OSCGenerator(),
|
||||
"ansible": AnsibleGenerator(),
|
||||
"rust-sdk": RustSdkGenerator(),
|
||||
"rust-cli": RustCliGenerator(),
|
||||
"openapi-spec": OpenApiSchemaGenerator(),
|
||||
"jsonschema": JsonSchemaGenerator(),
|
||||
"metadata": MetadataGenerator(),
|
||||
}
|
||||
|
||||
for g, v in generators.items():
|
||||
v.get_parser(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
generator = Generator()
|
||||
|
||||
if args.metadata:
|
||||
metadata_path = Path(args.metadata)
|
||||
generator.load_metadata(metadata_path)
|
||||
# Resulting mod_paths
|
||||
res_mods = []
|
||||
|
||||
for res, res_data in generator.metadata.resources.items():
|
||||
if args.service and not res.startswith(args.service):
|
||||
continue
|
||||
if args.resource and res != f"{args.service}.{args.resource}":
|
||||
continue
|
||||
for op, op_data in res_data.operations.items():
|
||||
logging.debug(f"Processing operation {op_data.operation_id}")
|
||||
if args.target in op_data.targets:
|
||||
op_args = op_data.targets[args.target]
|
||||
if not op_args.service_type:
|
||||
op_args.service_type = res.split(".")[0]
|
||||
if not op_args.api_version:
|
||||
op_args.api_version = res_data.api_version
|
||||
if not op_args.operation_type and op_data.operation_type:
|
||||
op_args.operation_type = op_data.operation_type
|
||||
# if not op_data.alternative_module_name and args.target == "rust-sdk":
|
||||
|
||||
openapi_spec = generator.get_openapi_spec(
|
||||
Path(
|
||||
# metadata_path.parent,
|
||||
op_data.spec_file
|
||||
or res_data.spec_file,
|
||||
).resolve()
|
||||
)
|
||||
|
||||
for mod_path, mod_name, path in generators[
|
||||
args.target
|
||||
].generate(
|
||||
res,
|
||||
args.work_dir,
|
||||
openapi_spec=openapi_spec,
|
||||
operation_id=op_data.operation_id,
|
||||
args=op_args,
|
||||
):
|
||||
res_mods.append((mod_path, mod_name, path))
|
||||
rust_sdk_extensions = res_data.extensions.get("rust-sdk")
|
||||
if rust_sdk_extensions:
|
||||
additional_modules = rust_sdk_extensions.setdefault(
|
||||
"additional_modules", []
|
||||
)
|
||||
res_x = res.split(".")
|
||||
for mod in additional_modules:
|
||||
res_mods.append(
|
||||
(
|
||||
[
|
||||
res_x[0].replace("-", "_"),
|
||||
res_data.api_version,
|
||||
res_x[1],
|
||||
],
|
||||
mod,
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
if args.target == "rust-sdk" and not args.resource:
|
||||
resource_results: dict[str, dict] = dict()
|
||||
for mod_path, mod_name, path in res_mods:
|
||||
mn = "/".join(mod_path)
|
||||
x = resource_results.setdefault(
|
||||
mn, {"path": path, "mods": set()}
|
||||
)
|
||||
x["mods"].add(mod_name)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for mod_path in [
|
||||
mod_path_str.split("/")
|
||||
for mod_path_str in resource_results.keys()
|
||||
]:
|
||||
if len(mod_path) < 3:
|
||||
continue
|
||||
mn = "/".join(mod_path[0:-1])
|
||||
mod_name = mod_path[-1]
|
||||
if mn in resource_results:
|
||||
if mod_name not in resource_results[mn]["mods"]:
|
||||
resource_results[mn]["mods"].add(mod_name)
|
||||
changed = True
|
||||
else:
|
||||
changed = True
|
||||
x = resource_results.setdefault(
|
||||
mn, {"path": path, "mods": set()}
|
||||
)
|
||||
x["mods"].add(mod_name)
|
||||
|
||||
for path, gen_data in resource_results.items():
|
||||
generators["rust-sdk"].generate_mod(
|
||||
args.work_dir,
|
||||
path.split("/"),
|
||||
gen_data["mods"],
|
||||
gen_data["path"],
|
||||
res.split(".")[-1].capitalize(),
|
||||
service_name=path.split("/")[0],
|
||||
)
|
||||
exit(0)
|
||||
|
||||
rp = None
|
||||
if args.module and args.class_name:
|
||||
rp = ResourceProcessor(args.module, args.class_name)
|
||||
|
||||
generators[args.target].generate(
|
||||
rp,
|
||||
args.work_dir,
|
||||
openapi_spec=None,
|
||||
operation_id=args.openapi_operation_id,
|
||||
args=args,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,443 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import re
|
||||
|
||||
import jsonref
|
||||
import yaml
|
||||
from openapi_core import Spec
|
||||
from pydantic import BaseModel
|
||||
|
||||
VERSION_RE = re.compile(r"[Vv][0-9.]*")
|
||||
# RE to split name from camelCase or by [`:`,`_`,`-`]
|
||||
SPLIT_NAME_RE = re.compile(r"(?<=[a-z])(?=[A-Z])|:|_|-")
|
||||
|
||||
# FullyQualifiedAttributeName alias map
|
||||
FQAN_ALIAS_MAP = {"network.floatingip.floating_ip_address": "name"}
|
||||
|
||||
|
||||
def _deep_merge(
|
||||
dict1: dict[Any, Any], dict2: dict[Any, Any]
|
||||
) -> dict[Any, Any]:
|
||||
result = dict1.copy()
|
||||
for key, value in dict2.items():
|
||||
if key in result:
|
||||
if isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = _deep_merge(result[key], value)
|
||||
continue
|
||||
elif isinstance(result[key], list) and isinstance(value, list):
|
||||
result[key] = result[key] + value
|
||||
continue
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class BasePrimitiveType(BaseModel):
|
||||
lifetimes: set[str] | None = None
|
||||
builder_macros: set[str] = set([])
|
||||
|
||||
|
||||
class BaseCombinedType(BaseModel):
|
||||
"""A Container Type (Array, Option)"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BaseCompoundType(BaseModel):
|
||||
"""A Complex Type (Enum/Struct)"""
|
||||
|
||||
name: str
|
||||
base_type: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
def get_openapi_spec(path: str | Path):
|
||||
"""Load OpenAPI spec from a file"""
|
||||
with open(path, "r") as fp:
|
||||
spec_data = jsonref.replace_refs(yaml.safe_load(fp), proxies=False)
|
||||
return Spec.from_dict(spec_data)
|
||||
|
||||
|
||||
def find_openapi_operation(spec, operationId: str):
|
||||
"""Find operation by operationId in the loaded spec"""
|
||||
for path, path_spec in spec["paths"].items():
|
||||
for method, method_spec in path_spec.items():
|
||||
if not isinstance(method_spec, dict):
|
||||
continue
|
||||
if method_spec.get("operationId") == operationId:
|
||||
return (path, method, method_spec)
|
||||
raise RuntimeError("Cannot find operation %s specification" % operationId)
|
||||
|
||||
|
||||
def get_plural_form(resource: str) -> str:
|
||||
"""Get plural for of the resource
|
||||
|
||||
Apply rules from https://www.fluentu.com/blog/english/plural-nouns/ to
|
||||
build a plural form of the word
|
||||
"""
|
||||
if resource[-1] == "y":
|
||||
return resource[0:-1] + "ies"
|
||||
elif resource[-1] == "o":
|
||||
return resource + "es"
|
||||
elif resource[-2:] == "is":
|
||||
return resource[0:-2] + "es"
|
||||
elif resource[-1] in ["s", "x", "z"] or resource[-2:] in ["sh", "ch"]:
|
||||
return resource + "es"
|
||||
elif resource[-1] == "f":
|
||||
return resource[:-1] + "ves"
|
||||
elif resource[-2:] == "fe":
|
||||
return resource[:-2] + "ves"
|
||||
else:
|
||||
return resource + "s"
|
||||
|
||||
|
||||
def get_singular_form(resource: str) -> str:
|
||||
"""Get singular for of the resource
|
||||
|
||||
Apply reverse rules from
|
||||
https://www.fluentu.com/blog/english/plural-nouns/ to build a singular
|
||||
plural form of the word keeping certain hacks
|
||||
"""
|
||||
if resource[-3:] == "ves":
|
||||
# impossible to reverse elf -> elves and knife -> knives
|
||||
return resource[0:-3] + "fe"
|
||||
elif resource[-3:] == "ies":
|
||||
return resource[0:-3] + "y"
|
||||
elif resource[-4:] == "sses":
|
||||
return resource[0:-2]
|
||||
elif resource[-2:] == "es":
|
||||
if resource[-4:-2] in ["sh", "ch"] or resource[-3] in ["s", "x", "z"]:
|
||||
return resource[0:-2]
|
||||
else:
|
||||
# it is impossible to reverse axis => axes
|
||||
return resource[0:-2]
|
||||
else:
|
||||
return resource[:-1]
|
||||
|
||||
|
||||
def find_resource_schema(
|
||||
schema: dict, parent: str | None = None, resource_name: str | None = None
|
||||
) -> tuple[dict | None, str | None]:
|
||||
"""Find the actual resource schema in the body schema
|
||||
|
||||
Traverse through the body schema searching for an element that represent
|
||||
the resource itself.
|
||||
|
||||
a) root is an object and it contain property with the resource name
|
||||
b) root is an object and it contain array property with name equals to
|
||||
the plural form of the resource name
|
||||
|
||||
:returns: tuple of (schema, attribute name) for the match or (None, None)
|
||||
if not found
|
||||
|
||||
"""
|
||||
try:
|
||||
if "type" not in schema:
|
||||
# Response of server create is a server or reservation_id
|
||||
# if "oneOf" in schema:
|
||||
# kinds = {}
|
||||
# for kind in schema["oneOf"]:
|
||||
# kinds.update(kind)
|
||||
# schema["type"] = kinds["type"]
|
||||
if "allOf" in schema:
|
||||
# {'allOf': [
|
||||
# {'type': 'integer', 'minimum': 0},
|
||||
# {'default': 0}]
|
||||
# }
|
||||
kinds = {}
|
||||
for kind in schema["allOf"]:
|
||||
kinds.update(kind)
|
||||
schema["type"] = kinds["type"]
|
||||
elif schema == {}:
|
||||
return (None, None)
|
||||
elif "properties" in schema:
|
||||
schema["type"] = "object"
|
||||
else:
|
||||
raise RuntimeError("No type in %s" % schema)
|
||||
schema_type = schema["type"]
|
||||
if schema_type == "array":
|
||||
if (
|
||||
parent
|
||||
and resource_name
|
||||
and parent == get_plural_form(resource_name)
|
||||
):
|
||||
items = schema["items"]
|
||||
if (
|
||||
items.get("type") == "object"
|
||||
and resource_name in items.get("properties", [])
|
||||
and len(items.get("properties", []).keys()) == 1
|
||||
):
|
||||
# Most likely this is Keypair where we have keypairs.keypair.{}
|
||||
return (items["properties"][resource_name], parent)
|
||||
else:
|
||||
return (items, parent)
|
||||
elif (
|
||||
not parent and schema.get("items", {}).get("type") == "object"
|
||||
):
|
||||
# Array on the top level. Most likely we are searching for items
|
||||
# directly
|
||||
return (schema["items"], None)
|
||||
return find_resource_schema(
|
||||
schema.get("items", {"type": "string"}),
|
||||
parent,
|
||||
resource_name=resource_name,
|
||||
)
|
||||
elif schema_type == "object":
|
||||
props = (
|
||||
schema.properties
|
||||
if hasattr(schema, "properties")
|
||||
else schema.get("properties", {})
|
||||
)
|
||||
if not parent and resource_name in props:
|
||||
# we are at the top level and there is property with the
|
||||
# resource name - it is what we are searching for
|
||||
el_type = props[resource_name]["type"]
|
||||
if el_type == "array":
|
||||
return (props[resource_name]["items"], resource_name)
|
||||
return (props[resource_name], resource_name)
|
||||
for name, item in props.items():
|
||||
if name == "additionalProperties" and isinstance(item, bool):
|
||||
# Some schemas are broken
|
||||
continue
|
||||
(r, path) = find_resource_schema(item, name, resource_name)
|
||||
if r:
|
||||
return (r, path)
|
||||
if not parent:
|
||||
# We are on top level and have not found anything.
|
||||
keys = list(props.keys())
|
||||
if len(keys) == 1:
|
||||
# there is only one field in the object
|
||||
if props[keys[0]].get("type") == "object":
|
||||
# and it is itself an object
|
||||
return (props[keys[0]], keys[0])
|
||||
else:
|
||||
# only field is not an object
|
||||
return (schema, None)
|
||||
else:
|
||||
return (schema, None)
|
||||
except Exception as ex:
|
||||
logging.exception(
|
||||
f"Caught exception {ex} during processing of {schema}"
|
||||
)
|
||||
raise
|
||||
return (None, None)
|
||||
|
||||
|
||||
def find_response_schema(
|
||||
responses: dict, response_key: str, action_name: str | None = None
|
||||
):
|
||||
"""Locate response schema
|
||||
|
||||
Some operations are having variety of possible responses (depending on
|
||||
microversion, action, etc). Try to locate suitable response for the client.
|
||||
|
||||
The function iterates over all defined responses and for 2** appies the
|
||||
following logic:
|
||||
|
||||
- if action_name is present AND oneOf is present AND action_name is in one
|
||||
of the oneOf schemas -> return this schema
|
||||
|
||||
- if action_name is not present AND oneOf is present AND response_key is in
|
||||
one of the OneOf candidates' properties (this is an object) -> return it
|
||||
|
||||
- action_name is not present AND oneOf is not present and (response_key or
|
||||
plural of the response_key) in candidate -> return it
|
||||
|
||||
:param dict responses: Dictionary with responses as defined in OpenAPI spec
|
||||
:param str response_key: Response key to be searching in responses (when
|
||||
aciton_name is not given) :param str action_name: Action name to be
|
||||
searching response for
|
||||
"""
|
||||
for code, rspec in responses.items():
|
||||
if not code.startswith("2"):
|
||||
continue
|
||||
content = rspec.get("content", {})
|
||||
if "application/json" in content:
|
||||
response_spec = content["application/json"]
|
||||
schema = response_spec["schema"]
|
||||
oneof = schema.get("oneOf")
|
||||
discriminator = schema.get("x-openstack", {}).get("discriminator")
|
||||
if oneof:
|
||||
if not discriminator:
|
||||
# Server create returns server or reservation info. For the
|
||||
# cli it is not very helpful and we look for response
|
||||
# candidate with the resource_name in the response
|
||||
for candidate in oneof:
|
||||
if (
|
||||
action_name
|
||||
and candidate.get("x-openstack", {}).get(
|
||||
"action-name"
|
||||
)
|
||||
== action_name
|
||||
):
|
||||
if response_key in candidate.get("properties", {}):
|
||||
# If there is a object with resource_name in
|
||||
# the props - this must be what we want to look
|
||||
# at
|
||||
return candidate["properties"][response_key]
|
||||
else:
|
||||
return candidate
|
||||
elif (
|
||||
not action_name
|
||||
and response_key
|
||||
and candidate.get("type") == "object"
|
||||
and response_key in candidate.get("properties", {})
|
||||
):
|
||||
# Actually for the sake of the CLI it may make
|
||||
# sense to merge all candidates
|
||||
return candidate["properties"][response_key]
|
||||
else:
|
||||
raise NotImplementedError
|
||||
elif (
|
||||
not action_name
|
||||
and schema
|
||||
and (
|
||||
response_key in schema
|
||||
or (
|
||||
schema.get("type") == "object"
|
||||
and (
|
||||
response_key in schema.get("properties", [])
|
||||
or get_plural_form(response_key)
|
||||
in schema.get("properties", [])
|
||||
)
|
||||
)
|
||||
)
|
||||
):
|
||||
return schema
|
||||
if not action_name:
|
||||
# Could not find anything with the given response_key. If there is any
|
||||
# 200/204 response - return it
|
||||
for code in ["200", "204"]:
|
||||
if code in responses:
|
||||
schema = (
|
||||
responses[code]
|
||||
.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema")
|
||||
)
|
||||
if schema and "type" in schema:
|
||||
return schema
|
||||
return None
|
||||
|
||||
|
||||
def get_resource_names_from_url(path: str):
|
||||
"""Construct Resource name from the URL"""
|
||||
path_elements = list(filter(None, path.split("/")))
|
||||
if path_elements and VERSION_RE.match(path_elements[0]):
|
||||
path_elements.pop(0)
|
||||
path_resource_names = []
|
||||
|
||||
for path_element in path_elements:
|
||||
if "{" not in path_element:
|
||||
el = path_element.replace("-", "_")
|
||||
if el[-3:] == "ies":
|
||||
part = el[0:-3] + "y"
|
||||
elif el[-4:] == "sses":
|
||||
part = el[0:-2]
|
||||
elif (
|
||||
el[-1] == "s"
|
||||
and el[-3:] != "dns"
|
||||
and el[-6:] != "access"
|
||||
and el != "qos"
|
||||
# quota/details
|
||||
and el != "details"
|
||||
):
|
||||
part = el[0:-1]
|
||||
else:
|
||||
part = el
|
||||
if part.startswith("os_"):
|
||||
# We should remove `os_` prefix from resource name
|
||||
part = part[3:]
|
||||
path_resource_names.append(part)
|
||||
if len(path_resource_names) > 1 and (
|
||||
path_resource_names[-1]
|
||||
in [
|
||||
"action",
|
||||
"detail",
|
||||
]
|
||||
or "add" in path_resource_names[-1]
|
||||
or "remove" in path_resource_names[-1]
|
||||
or "update" in path_resource_names[-1]
|
||||
):
|
||||
path_resource_names.pop()
|
||||
if len(path_resource_names) == 0:
|
||||
return ["version"]
|
||||
if path.startswith("/v2/schemas/"):
|
||||
# Image schemas should not be singularized (schema/images,
|
||||
# schema/image)
|
||||
path_resource_names[-1] = path_elements[-1]
|
||||
if path.startswith("/v2/images") and path.endswith("/actions/deactivate"):
|
||||
path_resource_names = ["image"]
|
||||
if path.startswith("/v2/images") and path.endswith("/actions/reactivate"):
|
||||
path_resource_names = ["image"]
|
||||
if path_resource_names == ["volume_transfer", "accept"]:
|
||||
path_resource_names = ["volume_transfer"]
|
||||
|
||||
return path_resource_names
|
||||
|
||||
|
||||
def get_rust_sdk_mod_path(service_type: str, api_version: str, path: str):
|
||||
"""Construct mod path for rust sdk"""
|
||||
mod_path = [
|
||||
service_type.replace("-", "_"),
|
||||
api_version,
|
||||
]
|
||||
mod_path.extend([x.lower() for x in get_resource_names_from_url(path)])
|
||||
return mod_path
|
||||
|
||||
|
||||
def get_rust_cli_mod_path(service_type: str, api_version: str, path: str):
|
||||
"""Construct mod path for rust sdk"""
|
||||
mod_path = [
|
||||
service_type.replace("-", "_"),
|
||||
api_version,
|
||||
]
|
||||
mod_path.extend([x.lower() for x in get_resource_names_from_url(path)])
|
||||
return mod_path
|
||||
|
||||
|
||||
def get_rust_service_type_from_str(xtype: str):
|
||||
match xtype:
|
||||
case "block-storage":
|
||||
return "BlockStorage"
|
||||
case "block_storage":
|
||||
return "BlockStorage"
|
||||
case "compute":
|
||||
return "Compute"
|
||||
case "identity":
|
||||
return "Identity"
|
||||
case "image":
|
||||
return "Image"
|
||||
case "network":
|
||||
return "Network"
|
||||
case "object-store":
|
||||
return "ObjectStore"
|
||||
case _:
|
||||
return xtype
|
||||
|
||||
|
||||
def make_ascii_string(description: str | None) -> str | None:
|
||||
"""Make sure a string is a valid ASCII charset
|
||||
|
||||
Placing a text with Unicode chars into the generated code may cause a lot
|
||||
of code sanity checks violations. Replace all known Unicode chars with
|
||||
something reasonable and return a pure ASCII string
|
||||
"""
|
||||
if not description:
|
||||
return None
|
||||
# PlusMinus - https://unicodeplus.com/U+00B1
|
||||
description = description.replace("\u00b1", "+-")
|
||||
|
||||
return description
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
# from openapi_core import Spec
|
||||
|
||||
|
||||
class TypeSchema(BaseModel):
|
||||
# TODO(gtema): enums are re-shuffled on every serialization
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
type: Optional[str | List[str]] = None
|
||||
format: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
summary: str | None = None
|
||||
default: Optional[Any] = None
|
||||
items: Optional[Dict[str, Any]] = None
|
||||
# circular reference cause issues on deserializing
|
||||
properties: Optional[Dict[str, Any]] = None
|
||||
nullable: Optional[bool] = None
|
||||
additionalProperties: Optional[bool | Any] = None
|
||||
|
||||
ref: Optional[str] = Field(alias="$ref", default=None)
|
||||
oneOf: Optional[List[Any]] = Field(default=None)
|
||||
anyOf: Optional[List[Any]] = Field(default=None)
|
||||
openstack: Optional[Dict[str, Any]] = Field(
|
||||
alias="x-openstack", default=None
|
||||
)
|
||||
required: Optional[List[str]] = None
|
||||
pattern: Optional[str] = None
|
||||
maxLength: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
def openapi_type_from_sdk(cls, type_name, fallback_type):
|
||||
if type_name in ["string", "str"]:
|
||||
return {"type": "string"}
|
||||
elif type_name == "int":
|
||||
return {"type": "integer"}
|
||||
elif type_name == "bool":
|
||||
return {"type": "boolean"}
|
||||
elif type_name == "dict":
|
||||
return {"type": "object"}
|
||||
elif type_name == "list":
|
||||
return {"type": "array"}
|
||||
else:
|
||||
# This is a fallback. Maybe we should define those objects
|
||||
return {"type": fallback_type}
|
||||
|
||||
@classmethod
|
||||
def from_sdk_field(cls, field, fallback_type="object"):
|
||||
property_schema_attrs = {}
|
||||
if field.type:
|
||||
field_type = getattr(field.type, "__name__", "string")
|
||||
else:
|
||||
field_type = "string"
|
||||
|
||||
property_schema_attrs.update(
|
||||
cls.openapi_type_from_sdk(field_type, fallback_type)
|
||||
)
|
||||
if field_type == "list":
|
||||
item_type = getattr(field, "list_type")
|
||||
item_type_str = getattr(item_type, "__name__", "string")
|
||||
property_schema_attrs["items"] = cls.openapi_type_from_sdk(
|
||||
item_type_str, fallback_type
|
||||
)
|
||||
|
||||
return cls(**property_schema_attrs)
|
||||
|
||||
|
||||
class ParameterSchema(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
location: str = Field(alias="in", default=None)
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
type_schema: TypeSchema = Field(alias="schema", default=None)
|
||||
required: bool = False
|
||||
deprecated: bool = False
|
||||
style: str | None = None
|
||||
explode: bool | None = None
|
||||
ref: str = Field(alias="$ref", default=None)
|
||||
openstack: Dict[str, Any] = Field(alias="x-openstack", default=None)
|
||||
|
||||
def get_sdk_name(self):
|
||||
return self.sdk_name or self.name
|
||||
|
||||
|
||||
class OperationSchema(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
parameters: List[ParameterSchema] = []
|
||||
description: str | None = None
|
||||
operationId: str | None = None
|
||||
requestBody: dict = {}
|
||||
responses: Dict[str, dict] = {}
|
||||
tags: List[str] = list()
|
||||
deprecated: bool | None = None
|
||||
openstack: dict = Field(alias="x-openstack", default={})
|
||||
security: List | None = None
|
||||
|
||||
|
||||
class HeaderSchema(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
description: Optional[str] = None
|
||||
openstack: Optional[Dict[str, Any]] = Field(
|
||||
alias="x-openstack", default=None
|
||||
)
|
||||
schema: Optional[TypeSchema] = Field(default=None)
|
||||
|
||||
|
||||
class PathSchema(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
parameters: List[ParameterSchema] = []
|
||||
get: OperationSchema = OperationSchema()
|
||||
post: OperationSchema = OperationSchema()
|
||||
delete: OperationSchema = OperationSchema()
|
||||
put: OperationSchema = OperationSchema()
|
||||
patch: OperationSchema = OperationSchema()
|
||||
head: OperationSchema = OperationSchema()
|
||||
|
||||
|
||||
class ComponentsSchema(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
schemas: Dict[str, TypeSchema] = {}
|
||||
parameters: Dict[str, ParameterSchema] = {}
|
||||
headers: Dict[str, HeaderSchema] = {}
|
||||
|
||||
|
||||
class SpecSchema(BaseModel):
|
||||
class Config:
|
||||
pupulate_by_name = True
|
||||
extra = "allow"
|
||||
|
||||
openapi: str
|
||||
info: dict
|
||||
paths: Dict[str, PathSchema] = {}
|
||||
components: ComponentsSchema = ComponentsSchema()
|
||||
tags: List[Dict] = []
|
||||
security: List[Dict] = []
|
||||
@@ -0,0 +1,85 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from codegenerator.base import BaseGenerator
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
|
||||
|
||||
class JsonSchemaGenerator(BaseGenerator):
|
||||
"""Generate jsonschema from the SDK resource"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def _build_resource_schema(self, res):
|
||||
# resource = res.resource_class
|
||||
properties = {}
|
||||
for k, v in res.attrs.items():
|
||||
field = v["attr"]
|
||||
properties[field.name] = TypeSchema.from_sdk_field(
|
||||
field
|
||||
).model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
)
|
||||
if "docs" in v:
|
||||
properties[field.name]["description"] = v["docs"]
|
||||
if k != field.name:
|
||||
properties[field.name]["x-openstack-sdk-name"] = k
|
||||
if k in [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"id",
|
||||
"status",
|
||||
"trunk_details",
|
||||
]:
|
||||
properties[field.name]["readOnly"] = True
|
||||
if k.startswith("min") or k.startswith("max") or "count" in k:
|
||||
properties[field.name]["type"] = "integer"
|
||||
if res.resource_class.resource_key:
|
||||
properties = {
|
||||
res.resource_class.resource_key: {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
}
|
||||
}
|
||||
schema = TypeSchema(
|
||||
type="object", properties=properties, description=""
|
||||
)
|
||||
# if res.resource_class._store_unknown_attrs_as_properties:
|
||||
# schema_attrs["additionalProperties"] = True
|
||||
# schema_attrs["properties"] = properties
|
||||
return schema
|
||||
|
||||
def generate(
|
||||
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
||||
):
|
||||
"""Generate Json Schema definition file for Resource"""
|
||||
logging.debug("Generating OpenAPI schema data")
|
||||
# We do not import generators since due to the use of Singletons in the
|
||||
# code importing glance, nova, cinder at the same time crashes
|
||||
# dramatically
|
||||
schema = self._build_resource_schema(res)
|
||||
print(
|
||||
json.dumps(
|
||||
json.loads(
|
||||
schema.model_dump_json(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
)
|
||||
),
|
||||
indent=4,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,740 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import re
|
||||
|
||||
import jsonref
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from codegenerator.base import BaseGenerator
|
||||
from codegenerator import common
|
||||
from codegenerator.common.schema import SpecSchema
|
||||
from codegenerator.types import Metadata
|
||||
from codegenerator.types import OperationModel
|
||||
from codegenerator.types import OperationTargetParams
|
||||
from codegenerator.types import ResourceModel
|
||||
|
||||
|
||||
class MetadataGenerator(BaseGenerator):
|
||||
"""Generate metadata from OpenAPI spec"""
|
||||
|
||||
def load_openapi(self, path):
|
||||
"""Load existing OpenAPI spec from the file"""
|
||||
if not path.exists():
|
||||
return
|
||||
yaml = YAML(typ="safe")
|
||||
with open(path, "r") as fp:
|
||||
spec = jsonref.replace_refs(yaml.load(fp))
|
||||
|
||||
return SpecSchema(**spec)
|
||||
|
||||
def generate(
|
||||
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
||||
):
|
||||
"""Generate Json Schema definition file for Resource"""
|
||||
logging.debug("Generating OpenAPI schema data")
|
||||
# We do not import generators since due to the use of Singletons in the
|
||||
# code importing glance, nova, cinder at the same time crashes
|
||||
# dramatically
|
||||
spec_path = Path(args.openapi_yaml_spec)
|
||||
metadata_path = Path(target_dir, args.service_type + "_metadata.yaml")
|
||||
|
||||
schema = self.load_openapi(spec_path)
|
||||
openapi_spec = common.get_openapi_spec(spec_path)
|
||||
metadata = Metadata(resources=dict())
|
||||
api_ver = "v" + schema.info["version"].split(".")[0]
|
||||
for path, spec in schema.paths.items():
|
||||
path_elements: list[str] = path.split("/")
|
||||
resource_name = "/".join(
|
||||
[x for x in common.get_resource_names_from_url(path)]
|
||||
)
|
||||
if args.service_type == "object-store":
|
||||
if path == "/v1/{account}":
|
||||
resource_name = "account"
|
||||
elif path == "/v1/{account}/{container}":
|
||||
resource_name = "container"
|
||||
if path == "/v1/{account}/{object}":
|
||||
resource_name = "object"
|
||||
if args.service_type == "compute" and resource_name in [
|
||||
"agent",
|
||||
"baremetal_node",
|
||||
"cell",
|
||||
"cell/capacity",
|
||||
"cell/info",
|
||||
"cell/sync_instance",
|
||||
"certificate",
|
||||
"cloudpipe",
|
||||
"fping",
|
||||
"fixed_ip",
|
||||
"floating_ip_dns",
|
||||
"floating_ip_dns/entry",
|
||||
"floating_ip_pool",
|
||||
"floating_ip_bulk",
|
||||
"host",
|
||||
"host/reboot",
|
||||
"host/shutdown",
|
||||
"host/startup",
|
||||
"image",
|
||||
"image/metadata",
|
||||
"network",
|
||||
"security_group_default_rule",
|
||||
"security_group_rule",
|
||||
"security_group",
|
||||
"server/console",
|
||||
"server/virtual_interface",
|
||||
"snapshot",
|
||||
"tenant_network",
|
||||
"volume",
|
||||
"volumes_boot",
|
||||
]:
|
||||
# We do not need to produce anything for deprecated APIs
|
||||
continue
|
||||
resource_model = metadata.resources.setdefault(
|
||||
f"{args.service_type}.{resource_name}",
|
||||
ResourceModel(
|
||||
api_version=api_ver,
|
||||
spec_file=spec_path.as_posix(),
|
||||
operations=dict(),
|
||||
),
|
||||
)
|
||||
for method in [
|
||||
"head",
|
||||
"get",
|
||||
"put",
|
||||
"post",
|
||||
"delete",
|
||||
"options",
|
||||
"patch",
|
||||
]:
|
||||
operation = getattr(spec, method, None)
|
||||
if operation:
|
||||
if not operation.operationId:
|
||||
# Every operation must have operationId
|
||||
continue
|
||||
op_model = OperationModel(
|
||||
operation_id=operation.operationId, targets=dict()
|
||||
)
|
||||
operation_key: str | None = None
|
||||
|
||||
response_schema: dict | None = None
|
||||
for code, rsp in operation.responses.items():
|
||||
if code.startswith("2"):
|
||||
response_schema = (
|
||||
rsp.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema", {})
|
||||
)
|
||||
break
|
||||
if path.endswith("}"):
|
||||
if method == "get":
|
||||
operation_key = "show"
|
||||
elif method == "head":
|
||||
operation_key = "check"
|
||||
elif method == "put":
|
||||
operation_key = "update"
|
||||
elif method == "patch":
|
||||
if (
|
||||
"application/json"
|
||||
in operation.requestBody.get("content", {})
|
||||
):
|
||||
operation_key = "update"
|
||||
else:
|
||||
operation_key = "patch"
|
||||
elif method == "post":
|
||||
operation_key = "create"
|
||||
elif method == "delete":
|
||||
operation_key = "delete"
|
||||
elif path.endswith("/detail"):
|
||||
if method == "get":
|
||||
operation_key = "list_detailed"
|
||||
# elif path.endswith("/default"):
|
||||
# operation_key = "default"
|
||||
elif path == "/v2/images/{image_id}/file":
|
||||
if method == "put":
|
||||
operation_key = "upload"
|
||||
elif method == "get":
|
||||
operation_key = "download"
|
||||
else:
|
||||
raise NotImplementedError
|
||||
elif path == "/v3/users/{user_id}/password":
|
||||
if method == "post":
|
||||
operation_key = "update"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
and resource_name == "flavor/flavor_access"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
and resource_name == "aggregate/image"
|
||||
and method == "post"
|
||||
):
|
||||
operation_key = "action"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
and resource_name == "server/security_group"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
and resource_name == "server/topology"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
|
||||
elif response_schema and (
|
||||
method == "get"
|
||||
and (
|
||||
response_schema.get("type", "") == "array"
|
||||
or (
|
||||
response_schema.get("type", "") == "object"
|
||||
and "properties" in response_schema
|
||||
and len(path_elements) > 1
|
||||
and path_elements[-1]
|
||||
in response_schema.get("properties", {})
|
||||
)
|
||||
)
|
||||
):
|
||||
# Response looks clearly like a list
|
||||
operation_key = "list"
|
||||
elif path.endswith("/action"):
|
||||
# Action
|
||||
operation_key = "action"
|
||||
elif args.service_type == "image" and path.endswith(
|
||||
"/actions/deactivate"
|
||||
):
|
||||
operation_key = "deactivate"
|
||||
elif args.service_type == "image" and path.endswith(
|
||||
"/actions/reactivate"
|
||||
):
|
||||
operation_key = "reactivate"
|
||||
elif (
|
||||
args.service_type == "block-storage"
|
||||
and "volume-transfers" in path
|
||||
and path.endswith("}/accept")
|
||||
):
|
||||
operation_key = "accept"
|
||||
elif (
|
||||
len(
|
||||
[
|
||||
x
|
||||
for x in schema.paths.keys()
|
||||
if x.startswith(path + "/{")
|
||||
]
|
||||
)
|
||||
> 0
|
||||
):
|
||||
# if we are at i.e. /v2/servers and there is
|
||||
# /v2/servers/{ most likely we are at the collection
|
||||
# level
|
||||
if method == "get":
|
||||
operation_key = "list"
|
||||
elif method == "head":
|
||||
operation_key = "check"
|
||||
elif method == "patch":
|
||||
if (
|
||||
"application/json"
|
||||
in operation.requestBody.get("content", {})
|
||||
):
|
||||
operation_key = "update"
|
||||
else:
|
||||
operation_key = "patch"
|
||||
elif method == "post":
|
||||
operation_key = "create"
|
||||
elif method == "put":
|
||||
operation_key = "replace"
|
||||
elif method == "delete":
|
||||
operation_key = "delete_all"
|
||||
elif method == "head":
|
||||
operation_key = "check"
|
||||
elif method == "get":
|
||||
operation_key = "get"
|
||||
elif method == "post":
|
||||
operation_key = "create"
|
||||
elif method == "put":
|
||||
operation_key = path.split("/")[-1]
|
||||
elif method == "patch":
|
||||
if "application/json" in operation.requestBody.get(
|
||||
"content", {}
|
||||
):
|
||||
operation_key = "update"
|
||||
else:
|
||||
operation_key = "patch"
|
||||
elif method == "delete":
|
||||
operation_key = "delete"
|
||||
if not operation_key:
|
||||
logging.warn(
|
||||
f"Cannot identify op name for {path}:{method}"
|
||||
)
|
||||
|
||||
# Next hacks
|
||||
if args.service_type == "identity" and resource_name in [
|
||||
"OS_FEDERATION/identity_provider",
|
||||
"OS_FEDERATION/identity_provider/protocol",
|
||||
"OS_FEDERATION/mapping",
|
||||
"OS_FEDERATION/service_provider",
|
||||
]:
|
||||
if method == "put":
|
||||
operation_key = "create"
|
||||
elif method == "patch":
|
||||
operation_key = "update"
|
||||
|
||||
if operation_key in resource_model:
|
||||
raise RuntimeError("Operation name conflict")
|
||||
else:
|
||||
if (
|
||||
operation_key == "action"
|
||||
and args.service_type
|
||||
in [
|
||||
"compute",
|
||||
"block-storage",
|
||||
]
|
||||
):
|
||||
# For action we actually have multiple independent operations
|
||||
try:
|
||||
body_schema = operation.requestBody["content"][
|
||||
"application/json"
|
||||
]["schema"]
|
||||
bodies = body_schema.get(
|
||||
"oneOf", [body_schema]
|
||||
)
|
||||
if len(bodies) > 1:
|
||||
discriminator = body_schema.get(
|
||||
"x-openstack", {}
|
||||
).get("discriminator")
|
||||
if discriminator != "action":
|
||||
raise RuntimeError(
|
||||
"Cannot generate metadata for %s since request body is not having action discriminator"
|
||||
% path
|
||||
)
|
||||
for body in bodies:
|
||||
action_name = body.get(
|
||||
"x-openstack", {}
|
||||
).get("action-name")
|
||||
if not action_name:
|
||||
action_name = list(
|
||||
body["properties"].keys()
|
||||
)[0]
|
||||
# Hardcode fixes
|
||||
if (
|
||||
resource_name == "flavor"
|
||||
and action_name
|
||||
in [
|
||||
"update",
|
||||
"create",
|
||||
"delete",
|
||||
]
|
||||
):
|
||||
# Flavor update/create/delete
|
||||
# operations are exposed ALSO as wsgi
|
||||
# actions. This is wrong and useless.
|
||||
logging.warn(
|
||||
"Skipping generating %s:%s action",
|
||||
resource_name,
|
||||
action_name,
|
||||
)
|
||||
continue
|
||||
|
||||
operation_name = "-".join(
|
||||
x.lower()
|
||||
for x in re.split(
|
||||
common.SPLIT_NAME_RE, action_name
|
||||
)
|
||||
).lower()
|
||||
rust_sdk_params = (
|
||||
get_rust_sdk_operation_args(
|
||||
"action",
|
||||
operation_name=action_name,
|
||||
module_name=get_module_name(
|
||||
action_name
|
||||
),
|
||||
)
|
||||
)
|
||||
rust_cli_params = (
|
||||
get_rust_cli_operation_args(
|
||||
"action",
|
||||
operation_name=action_name,
|
||||
module_name=get_module_name(
|
||||
action_name
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
op_model = OperationModel(
|
||||
operation_id=operation.operationId,
|
||||
targets=dict(),
|
||||
)
|
||||
op_model.operation_type = "action"
|
||||
|
||||
op_model.targets["rust-sdk"] = (
|
||||
rust_sdk_params
|
||||
)
|
||||
op_model.targets["rust-cli"] = (
|
||||
rust_cli_params
|
||||
)
|
||||
|
||||
op_model = post_process_operation(
|
||||
args.service_type,
|
||||
resource_name,
|
||||
operation_name,
|
||||
op_model,
|
||||
)
|
||||
|
||||
resource_model.operations[
|
||||
operation_name
|
||||
] = op_model
|
||||
|
||||
except KeyError:
|
||||
raise RuntimeError(
|
||||
"Cannot get bodies for %s" % path
|
||||
)
|
||||
else:
|
||||
if not operation_key:
|
||||
raise NotImplementedError
|
||||
operation_type = get_operation_type_by_key(
|
||||
operation_key
|
||||
)
|
||||
op_model.operation_type = operation_type
|
||||
# NOTE: sdk gets operation_key and not operation_type
|
||||
rust_sdk_params = get_rust_sdk_operation_args(
|
||||
operation_key
|
||||
)
|
||||
rust_cli_params = get_rust_cli_operation_args(
|
||||
operation_key
|
||||
)
|
||||
|
||||
op_model.targets["rust-sdk"] = rust_sdk_params
|
||||
if rust_cli_params and not (
|
||||
args.service_type == "identity"
|
||||
and operation_key == "check"
|
||||
):
|
||||
op_model.targets["rust-cli"] = rust_cli_params
|
||||
|
||||
op_model = post_process_operation(
|
||||
args.service_type,
|
||||
resource_name,
|
||||
operation_key,
|
||||
op_model,
|
||||
)
|
||||
|
||||
resource_model.operations[operation_key] = op_model
|
||||
pass
|
||||
for res_name, res_data in metadata.resources.items():
|
||||
# Sanitize produced metadata
|
||||
list_op = res_data.operations.get("list")
|
||||
list_detailed_op = res_data.operations.get("list_detailed")
|
||||
if list_op and list_detailed_op:
|
||||
# There are both plain list and list with details operation.
|
||||
# For the certain generator backend it makes no sense to have
|
||||
# then both so we should disable generation of certain backends
|
||||
# for the non detailed endpoint
|
||||
list_op.targets.pop("rust-cli")
|
||||
|
||||
# Prepare `find` operation data
|
||||
if (list_op or list_detailed_op) and res_data.operations.get(
|
||||
"show"
|
||||
):
|
||||
show_op = res_data.operations["show"]
|
||||
|
||||
(path, _, spec) = common.find_openapi_operation(
|
||||
openapi_spec, show_op.operation_id
|
||||
)
|
||||
mod_path = common.get_rust_sdk_mod_path(
|
||||
args.service_type,
|
||||
res_data.api_version or "",
|
||||
path,
|
||||
)
|
||||
response_schema = None
|
||||
for code, rspec in spec.get("responses", {}).items():
|
||||
if not code.startswith("2"):
|
||||
continue
|
||||
content = rspec.get("content", {})
|
||||
if "application/json" in content:
|
||||
try:
|
||||
(
|
||||
response_schema,
|
||||
_,
|
||||
) = common.find_resource_schema(
|
||||
content["application/json"].get("schema", {}),
|
||||
None,
|
||||
)
|
||||
except Exception as ex:
|
||||
logging.exception(
|
||||
"Cannot process response of %s operation: %s",
|
||||
show_op.operation_id,
|
||||
ex,
|
||||
)
|
||||
|
||||
if not response_schema:
|
||||
# Show does not have a suitable
|
||||
# response. We can't have find
|
||||
# for such
|
||||
continue
|
||||
if "id" not in response_schema.get("properties", {}).keys():
|
||||
# Resource has no ID in show method => find impossible
|
||||
continue
|
||||
elif (
|
||||
"name" not in response_schema.get("properties", {}).keys()
|
||||
and res_name != "floatingip"
|
||||
):
|
||||
# Resource has no NAME => find useless
|
||||
continue
|
||||
|
||||
list_op_ = list_detailed_op or list_op
|
||||
if not list_op_:
|
||||
continue
|
||||
(_, _, list_spec) = common.find_openapi_operation(
|
||||
openapi_spec, list_op_.operation_id
|
||||
)
|
||||
name_field: str = "name"
|
||||
for fqan, alias in common.FQAN_ALIAS_MAP.items():
|
||||
if fqan.startswith(res_name) and alias == "name":
|
||||
name_field = fqan.split(".")[-1]
|
||||
name_filter_supported: bool = False
|
||||
if name_field in [
|
||||
x.get("name")
|
||||
for x in list(list_spec.get("parameters", []))
|
||||
]:
|
||||
name_filter_supported = True
|
||||
|
||||
sdk_params = OperationTargetParams(
|
||||
module_name="find",
|
||||
name_field=name_field,
|
||||
name_filter_supported=name_filter_supported,
|
||||
sdk_mod_path="::".join(mod_path),
|
||||
list_mod="list_detailed" if list_detailed_op else "list",
|
||||
)
|
||||
res_data.operations["find"] = OperationModel(
|
||||
operation_id=list_op_.operation_id,
|
||||
operation_type="find",
|
||||
targets={"rust-sdk": sdk_params},
|
||||
)
|
||||
|
||||
# Let other operations know of `find` presence
|
||||
for op_name, op_data in res_data.operations.items():
|
||||
if op_name not in ["find", "list", "create"]:
|
||||
for (
|
||||
target_name,
|
||||
target_params,
|
||||
) in op_data.targets.items():
|
||||
if target_name in ["rust-cli"]:
|
||||
target_params.find_implemented_by_sdk = True
|
||||
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
yaml.default_flow_style = False
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
metadata_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
with open(metadata_path, "w") as fp:
|
||||
yaml.dump(
|
||||
metadata.model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
),
|
||||
fp,
|
||||
)
|
||||
|
||||
|
||||
def get_operation_type_by_key(operation_key):
|
||||
if operation_key in ["list", "list_detailed"]:
|
||||
return "list"
|
||||
elif operation_key == "get":
|
||||
return "get"
|
||||
elif operation_key == "check":
|
||||
return "get"
|
||||
elif operation_key == "show":
|
||||
return "show"
|
||||
elif operation_key in ["update", "replace"]:
|
||||
return "set"
|
||||
elif operation_key in ["delete", "delete_all"]:
|
||||
return "delete"
|
||||
elif operation_key in ["create"]:
|
||||
return "create"
|
||||
elif operation_key == "patch":
|
||||
return "set"
|
||||
elif operation_key == "default":
|
||||
return "get"
|
||||
elif operation_key == "download":
|
||||
return "download"
|
||||
elif operation_key == "upload":
|
||||
return "upload"
|
||||
else:
|
||||
return "action"
|
||||
|
||||
|
||||
def get_rust_sdk_operation_args(
|
||||
operation_key: str,
|
||||
operation_name: str | None = None,
|
||||
module_name: str | None = None,
|
||||
):
|
||||
"""Construct proper Rust SDK parameters for operation by type"""
|
||||
sdk_params = OperationTargetParams()
|
||||
sdk_params.module_name = module_name
|
||||
if operation_key == "show":
|
||||
sdk_params.module_name = "get"
|
||||
elif operation_key == "list_detailed":
|
||||
sdk_params.module_name = "list_detailed"
|
||||
# elif operation_key == "action" and not module_name:
|
||||
# sdk_params.module_name = operation_name if operation_name else operation_key
|
||||
else:
|
||||
sdk_params.module_name = module_name or get_module_name(
|
||||
# get_operation_type_by_key(operation_key)
|
||||
operation_key
|
||||
)
|
||||
sdk_params.operation_name = operation_name
|
||||
|
||||
return sdk_params
|
||||
|
||||
|
||||
def get_rust_cli_operation_args(
|
||||
operation_key: str,
|
||||
operation_name: str | None = None,
|
||||
module_name: str | None = None,
|
||||
):
|
||||
"""Construct proper Rust CLI parameters for operation by type"""
|
||||
# Get SDK params to connect things with each other
|
||||
# operation_type = get_operation_type_by_key(operation_key)
|
||||
sdk_params = get_rust_sdk_operation_args(
|
||||
operation_key, operation_name=operation_name, module_name=module_name
|
||||
)
|
||||
cli_params = OperationTargetParams()
|
||||
cli_params.sdk_mod_name = sdk_params.module_name
|
||||
cli_params.module_name = module_name or get_module_name(operation_key)
|
||||
cli_params.operation_name = operation_name
|
||||
|
||||
return cli_params
|
||||
|
||||
|
||||
def get_module_name(name):
|
||||
if name in ["list", "list_detailed"]:
|
||||
return "list"
|
||||
elif name == "get":
|
||||
return "get"
|
||||
elif name == "show":
|
||||
return "show"
|
||||
elif name == "check":
|
||||
return "head"
|
||||
elif name == "update":
|
||||
return "set"
|
||||
elif name == "replace":
|
||||
return "replace"
|
||||
elif name == "delete":
|
||||
return "delete"
|
||||
elif name == "delete_all":
|
||||
return "delete_all"
|
||||
elif name in ["create"]:
|
||||
return "create"
|
||||
elif name in ["default"]:
|
||||
return "default"
|
||||
return "_".join(x.lower() for x in re.split(common.SPLIT_NAME_RE, name))
|
||||
|
||||
|
||||
def post_process_operation(
|
||||
service_type: str, resource_name: str, operation_name: str, operation
|
||||
):
|
||||
if service_type == "compute":
|
||||
operation = post_process_compute_operation(
|
||||
resource_name, operation_name, operation
|
||||
)
|
||||
elif service_type == "identity":
|
||||
operation = post_process_identity_operation(
|
||||
resource_name, operation_name, operation
|
||||
)
|
||||
elif service_type == "image":
|
||||
operation = post_process_image_operation(
|
||||
resource_name, operation_name, operation
|
||||
)
|
||||
elif service_type in ["block-storage", "volume"]:
|
||||
operation = post_process_block_storage_operation(
|
||||
resource_name, operation_name, operation
|
||||
)
|
||||
return operation
|
||||
|
||||
|
||||
def post_process_compute_operation(
|
||||
resource_name: str, operation_name: str, operation
|
||||
):
|
||||
if resource_name == "aggregate":
|
||||
if operation_name in ["set-metadata", "add-host", "remove-host"]:
|
||||
operation.targets["rust-sdk"].response_key = "aggregate"
|
||||
operation.targets["rust-cli"].response_key = "aggregate"
|
||||
elif resource_name == "availability_zone":
|
||||
if operation_name in ["get", "list_detailed"]:
|
||||
operation.targets["rust-sdk"].response_key = "availabilityZoneInfo"
|
||||
operation.targets["rust-cli"].response_key = "availabilityZoneInfo"
|
||||
elif resource_name == "keypair":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-sdk"].response_list_item_key = "keypair"
|
||||
elif resource_name == "server/instance_action":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-sdk"].response_key = "instanceActions"
|
||||
operation.targets["rust-cli"].response_key = "instanceActions"
|
||||
else:
|
||||
operation.targets["rust-sdk"].response_key = "instanceAction"
|
||||
operation.targets["rust-cli"].response_key = "instanceAction"
|
||||
elif resource_name == "server/topology":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-sdk"].response_key = "nodes"
|
||||
operation.targets["rust-cli"].response_key = "nodes"
|
||||
elif resource_name == "server/volume_attachment":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-sdk"].response_key = "volumeAttachments"
|
||||
operation.targets["rust-cli"].response_key = "volumeAttachments"
|
||||
elif operation_name in ["create", "show", "update"]:
|
||||
operation.targets["rust-sdk"].response_key = "volumeAttachment"
|
||||
operation.targets["rust-cli"].response_key = "volumeAttachment"
|
||||
|
||||
return operation
|
||||
|
||||
|
||||
def post_process_identity_operation(
|
||||
resource_name: str, operation_name: str, operation
|
||||
):
|
||||
if resource_name == "role/imply":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-cli"].response_key = "role_inference"
|
||||
operation.targets["rust-sdk"].response_key = "role_inference"
|
||||
if resource_name == "role_inference":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-cli"].response_key = "role_inferences"
|
||||
operation.targets["rust-sdk"].response_key = "role_inferences"
|
||||
return operation
|
||||
|
||||
|
||||
def post_process_image_operation(
|
||||
resource_name: str, operation_name: str, operation
|
||||
):
|
||||
if resource_name.startswith("schema"):
|
||||
# Image schemas are a JSON operation
|
||||
operation.targets["rust-cli"].operation_type = "json"
|
||||
|
||||
return operation
|
||||
|
||||
|
||||
def post_process_block_storage_operation(
|
||||
resource_name: str, operation_name: str, operation
|
||||
):
|
||||
if resource_name == "type":
|
||||
if operation_name == "list":
|
||||
operation.targets["rust-cli"].response_key = "volume_types"
|
||||
operation.targets["rust-sdk"].response_key = "volume_types"
|
||||
elif operation_name in ["create", "show", "update"]:
|
||||
operation.targets["rust-cli"].response_key = "volume_type"
|
||||
operation.targets["rust-sdk"].response_key = "volume_type"
|
||||
elif resource_name == "type/volume_type_access":
|
||||
operation.targets["rust-cli"].response_key = "volume_type_access"
|
||||
operation.targets["rust-sdk"].response_key = "volume_type_access"
|
||||
|
||||
return operation
|
||||
@@ -0,0 +1,666 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Type
|
||||
import typing as ty
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from codegenerator import common
|
||||
|
||||
|
||||
def dicthash_(data: dict[str, Any]) -> str:
|
||||
"""Calculate hash of the dictionary"""
|
||||
dh = hashlib.md5()
|
||||
encoded = json.dumps(data, sort_keys=True).encode()
|
||||
dh.update(encoded)
|
||||
return dh.hexdigest()
|
||||
|
||||
|
||||
class Reference(BaseModel):
|
||||
"""Reference of the complex type to the occurence instance"""
|
||||
|
||||
#: Name of the object that uses the type under reference
|
||||
name: str
|
||||
type: Type | None = None
|
||||
hash_: str | None = None
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.type, self.hash_))
|
||||
|
||||
|
||||
class PrimitiveType(BaseModel):
|
||||
"""Primitive Data Type stricture"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PrimitiveString(PrimitiveType):
|
||||
pass
|
||||
|
||||
|
||||
class ConstraintString(PrimitiveType):
|
||||
format: str | None = None
|
||||
minLength: int | None = None
|
||||
maxLength: int | None = None
|
||||
pattern: str | None = None
|
||||
enum: list[Any] | None = None
|
||||
|
||||
|
||||
class PrimitiveNumber(PrimitiveType):
|
||||
pass
|
||||
|
||||
|
||||
class ConstraintNumber(PrimitiveNumber):
|
||||
format: str | None = None
|
||||
minimum: int | None = None
|
||||
maximum: int | float | None = None
|
||||
exclusiveMaximum: bool | None = None
|
||||
multipleOf: int | float | None = None
|
||||
|
||||
|
||||
class ConstraintInteger(ConstraintNumber):
|
||||
pass
|
||||
|
||||
|
||||
class PrimitiveBoolean(PrimitiveType):
|
||||
pass
|
||||
|
||||
|
||||
class PrimitiveNull(PrimitiveType):
|
||||
pass
|
||||
|
||||
|
||||
class PrimitiveAny(PrimitiveType):
|
||||
pass
|
||||
|
||||
|
||||
class ADT(BaseModel):
|
||||
"""Abstract Data Type / Composite - typically sort of
|
||||
collection of Primitives"""
|
||||
|
||||
reference: Reference | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class AbstractList(ADT):
|
||||
"""Abstract list"""
|
||||
|
||||
item_type: PrimitiveType | ADT | Reference
|
||||
|
||||
|
||||
class AbstractCollection(ADT):
|
||||
"""AllOf/OneOf/etc"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractContainer(ADT):
|
||||
"""Struct/Object"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OneOfType(ADT):
|
||||
"""OneOf - a collection of data types where only one of the kinds can be used (a.k.a. enum)"""
|
||||
|
||||
kinds: list[PrimitiveType | ADT | Reference] = []
|
||||
|
||||
|
||||
class Enum(AbstractCollection):
|
||||
"""Enum: a unique collection of primitives"""
|
||||
|
||||
base_types: list[Type[PrimitiveType]] = []
|
||||
literals: set[Any] = set()
|
||||
|
||||
|
||||
class StructField(BaseModel):
|
||||
"""Structure field: type + additional info"""
|
||||
|
||||
data_type: PrimitiveType | ADT | Reference
|
||||
description: str | None = None
|
||||
is_required: bool = False
|
||||
min_ver: str | None = None
|
||||
max_ver: str | None = None
|
||||
|
||||
|
||||
class Struct(ADT):
|
||||
"""Struct/Object"""
|
||||
|
||||
fields: dict[str, StructField] = {}
|
||||
additional_fields: PrimitiveType | ADT | None = None
|
||||
pattern_properties: dict[str, PrimitiveType | ADT] | None = None
|
||||
|
||||
|
||||
class Dictionary(ADT):
|
||||
"""Simple dictionary with values of a single type"""
|
||||
|
||||
value_type: PrimitiveType | ADT
|
||||
|
||||
|
||||
class Array(AbstractList):
|
||||
"""A pure list"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommaSeparatedList(AbstractList):
|
||||
"""A list that is serialized comma separated"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Set(AbstractList):
|
||||
"""A set of unique items"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JsonSchemaParser:
|
||||
"""JsonSchema to internal DataModel converter"""
|
||||
|
||||
def parse(
|
||||
self, schema, ignore_read_only: bool = False
|
||||
) -> ty.Tuple[ADT | None, list[ADT]]:
|
||||
"""Parse JsonSchema object into internal DataModel"""
|
||||
results: list[ADT] = []
|
||||
res = self.parse_schema(
|
||||
schema, results, ignore_read_only=ignore_read_only
|
||||
)
|
||||
return (res, results)
|
||||
|
||||
def parse_schema(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
min_ver: str | None = None,
|
||||
max_ver: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
) -> PrimitiveType | ADT:
|
||||
type_ = schema.get("type")
|
||||
if "oneOf" in schema:
|
||||
return self.parse_oneOf(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif "enum" in schema:
|
||||
return self.parse_enum(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif "allOf" in schema:
|
||||
return self.parse_allOf(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif isinstance(type_, list):
|
||||
return self.parse_typelist(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif isinstance(type_, str):
|
||||
if type_ == "object":
|
||||
return self.parse_object(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
min_ver=min_ver,
|
||||
max_ver=max_ver,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif type_ == "array":
|
||||
return self.parse_array(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif type_ == "string":
|
||||
obj = ConstraintString(**schema)
|
||||
# todo: set obj props
|
||||
return obj
|
||||
elif type_ == "integer":
|
||||
obj = ConstraintInteger(**schema)
|
||||
# todo: set obj props
|
||||
return obj
|
||||
elif type_ == "number":
|
||||
obj = ConstraintNumber(**schema)
|
||||
# todo: set obj props
|
||||
return obj
|
||||
elif type_ == "boolean":
|
||||
obj = PrimitiveBoolean()
|
||||
# todo: set obj props
|
||||
return obj
|
||||
elif type_ == "null":
|
||||
obj = PrimitiveNull()
|
||||
return obj
|
||||
elif not type_ and "properties" in schema:
|
||||
# Sometimes services forget to set "type=object"
|
||||
return self.parse_object(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
parent_name=parent_name,
|
||||
min_ver=min_ver,
|
||||
max_ver=max_ver,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
elif schema == {}:
|
||||
return PrimitiveNull()
|
||||
elif not type_ and "format" in schema:
|
||||
return ConstraintString(**schema)
|
||||
raise RuntimeError("Cannot determine type for %s", schema)
|
||||
|
||||
def parse_object(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
min_ver: str | None = None,
|
||||
max_ver: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
obj: ADT | None = None
|
||||
properties = schema.get("properties")
|
||||
additional_properties = schema.get("additionalProperties")
|
||||
additional_properties_type: PrimitiveType | ADT | None = None
|
||||
pattern_properties = schema.get("patternProperties")
|
||||
pattern_props: dict[str, PrimitiveType | ADT] | None = {}
|
||||
required = schema.get("required", [])
|
||||
os_ext: dict = schema.get("x-openstack", {})
|
||||
min_ver = os_ext.get("min-ver", min_ver)
|
||||
max_ver = os_ext.get("max-ver", max_ver)
|
||||
if properties:
|
||||
obj = Struct()
|
||||
for k, v in properties.items():
|
||||
if k == "additionalProperties" and isinstance(v, bool):
|
||||
# Some schemas (in keystone) are Broken
|
||||
continue
|
||||
if ignore_read_only and v.get("readOnly", False):
|
||||
continue
|
||||
data_type = self.parse_schema(
|
||||
v,
|
||||
results,
|
||||
name=k,
|
||||
parent_name=name,
|
||||
min_ver=min_ver,
|
||||
max_ver=max_ver,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
ref = getattr(data_type, "reference", None)
|
||||
if ref:
|
||||
field = StructField(data_type=ref)
|
||||
else:
|
||||
field = StructField(
|
||||
data_type=data_type,
|
||||
)
|
||||
|
||||
field.description = v.get("description")
|
||||
if k in required:
|
||||
field.is_required = True
|
||||
if min_ver:
|
||||
field.min_ver = min_ver
|
||||
if max_ver:
|
||||
field.max_ver = max_ver
|
||||
obj.fields[k] = field
|
||||
if additional_properties:
|
||||
if (
|
||||
isinstance(additional_properties, dict)
|
||||
and "type" in additional_properties
|
||||
):
|
||||
additional_properties_type = self.parse_schema(
|
||||
additional_properties,
|
||||
results,
|
||||
name=name,
|
||||
min_ver=min_ver,
|
||||
max_ver=max_ver,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
else:
|
||||
additional_properties_type = PrimitiveAny()
|
||||
|
||||
if pattern_properties:
|
||||
for key_pattern, value_type in pattern_properties.items():
|
||||
type_kind: PrimitiveType | ADT = self.parse_schema(
|
||||
value_type,
|
||||
results,
|
||||
name=name,
|
||||
min_ver=min_ver,
|
||||
max_ver=max_ver,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
pattern_props[key_pattern] = type_kind # type: ignore
|
||||
|
||||
if obj:
|
||||
if additional_properties_type:
|
||||
obj.additional_fields = additional_properties_type
|
||||
if pattern_props:
|
||||
obj.pattern_properties = copy.deepcopy(pattern_props)
|
||||
else:
|
||||
if pattern_props and not additional_properties_type:
|
||||
if len(list(pattern_props.values())) == 1:
|
||||
obj = Dictionary(
|
||||
value_type=list(pattern_props.values())[0]
|
||||
)
|
||||
else:
|
||||
obj = Struct(pattern_properties=pattern_props)
|
||||
elif not pattern_props and additional_properties_type:
|
||||
obj = Dictionary(value_type=additional_properties_type)
|
||||
else:
|
||||
obj = Dictionary(value_type=PrimitiveAny())
|
||||
if not obj:
|
||||
raise RuntimeError("Object %s is not supported", schema)
|
||||
|
||||
if name:
|
||||
obj.reference = Reference(
|
||||
name=name, type=obj.__class__, hash_=dicthash_(schema)
|
||||
)
|
||||
|
||||
if obj:
|
||||
obj.description = schema.get("description")
|
||||
if (
|
||||
obj.reference
|
||||
and f"{obj.reference.name}{obj.reference.type}"
|
||||
in [
|
||||
f"{x.reference.name}{x.reference.type}"
|
||||
for x in results
|
||||
if x.reference
|
||||
]
|
||||
):
|
||||
# Structure with the same name is already present. Prefix the
|
||||
# new one with the parent name
|
||||
if parent_name and name:
|
||||
new_name = parent_name + "_" + name
|
||||
|
||||
if Reference(name=new_name, type=obj.reference.type) in [
|
||||
x.reference for x in results
|
||||
]:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
obj.reference.name = new_name
|
||||
results.append(obj)
|
||||
return obj
|
||||
|
||||
def parse_oneOf(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
obj = OneOfType()
|
||||
for kind in schema.get("oneOf"):
|
||||
kind_schema = common._deep_merge(schema, kind)
|
||||
kind_schema.pop("oneOf")
|
||||
# todo: merge base props into the kind
|
||||
kind_type = self.parse_schema(
|
||||
kind_schema,
|
||||
results,
|
||||
name=name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
if not kind_type:
|
||||
raise NotImplementedError
|
||||
ref: Reference | None = getattr(kind_type, "reference", None)
|
||||
if ref:
|
||||
obj.kinds.append(ref)
|
||||
else:
|
||||
obj.kinds.append(kind_type)
|
||||
if name:
|
||||
obj.reference = Reference(
|
||||
name=name, type=obj.__class__, hash_=dicthash_(schema)
|
||||
)
|
||||
results.append(obj)
|
||||
return obj
|
||||
|
||||
def parse_typelist(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
if len(schema.get("type")) == 1:
|
||||
# Bad schema with type being a list of 1 entry
|
||||
schema["type"] = schema["type"][0]
|
||||
obj = self.parse_schema(
|
||||
schema,
|
||||
results,
|
||||
name=name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
return obj
|
||||
|
||||
obj = OneOfType()
|
||||
|
||||
for kind_type in schema.get("type"):
|
||||
kind_schema = copy.deepcopy(schema)
|
||||
kind_schema["type"] = kind_type
|
||||
kind_type = self.parse_schema(
|
||||
kind_schema,
|
||||
results,
|
||||
name=name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
ref = getattr(kind_type, "reference", None)
|
||||
if ref:
|
||||
obj.kinds.append(ref)
|
||||
else:
|
||||
obj.kinds.append(kind_type)
|
||||
if name:
|
||||
obj.reference = Reference(
|
||||
name=name, type=obj.__class__, hash_=dicthash_(schema)
|
||||
)
|
||||
results.append(obj)
|
||||
return obj
|
||||
|
||||
def parse_array(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
# todo: decide whether some constraints can be under items
|
||||
item_type = self.parse_schema(
|
||||
schema.get("items", {"type": "string"}),
|
||||
results,
|
||||
name=name,
|
||||
ignore_read_only=ignore_read_only,
|
||||
)
|
||||
ref = getattr(item_type, "reference", None)
|
||||
if ref:
|
||||
obj = Array(item_type=ref)
|
||||
else:
|
||||
obj = Array(item_type=item_type)
|
||||
if name:
|
||||
obj.reference = Reference(
|
||||
name=name, type=obj.__class__, hash_=dicthash_(schema)
|
||||
)
|
||||
results.append(obj)
|
||||
return obj
|
||||
|
||||
def parse_enum(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
# todo: decide whether some constraints can be under items
|
||||
literals = schema.get("enum")
|
||||
obj = Enum(literals=literals, base_types=[])
|
||||
literal_types = set([type(x) for x in literals])
|
||||
for literal_type in literal_types:
|
||||
if literal_type is str:
|
||||
obj.base_types.append(ConstraintString)
|
||||
elif literal_type is int:
|
||||
obj.base_types.append(ConstraintInteger)
|
||||
elif literal_type is bool:
|
||||
obj.base_types.append(PrimitiveBoolean)
|
||||
|
||||
if name:
|
||||
obj.reference = Reference(
|
||||
name=name, type=obj.__class__, hash_=dicthash_(schema)
|
||||
)
|
||||
results.append(obj)
|
||||
return obj
|
||||
|
||||
def parse_allOf(
|
||||
self,
|
||||
schema,
|
||||
results: list[ADT],
|
||||
name: str | None = None,
|
||||
parent_name: str | None = None,
|
||||
ignore_read_only: bool | None = False,
|
||||
):
|
||||
sch = copy.deepcopy(schema)
|
||||
sch.pop("allOf")
|
||||
for kind in schema.get("allOf"):
|
||||
sch = common._deep_merge(sch, kind)
|
||||
obj = self.parse_schema(
|
||||
sch, results, name=name, ignore_read_only=ignore_read_only
|
||||
)
|
||||
if not obj:
|
||||
raise NotImplementedError
|
||||
# if name:
|
||||
# obj.reference = Reference(name=name, type=obj.__class__)
|
||||
# results.append(obj)
|
||||
return obj
|
||||
|
||||
|
||||
class RequestParameter(BaseModel):
|
||||
"""OpenAPI Request parameter DataType wrapper"""
|
||||
|
||||
name: str
|
||||
location: str
|
||||
data_type: PrimitiveType | ADT
|
||||
description: str | None = None
|
||||
is_required: bool = False
|
||||
is_flag: bool = False
|
||||
|
||||
|
||||
class OpenAPISchemaParser(JsonSchemaParser):
|
||||
"""OpenAPI to internal DataModel converter"""
|
||||
|
||||
def parse_parameter(self, schema) -> RequestParameter:
|
||||
"""Parse OpenAPI request parameter into internal DataModel"""
|
||||
param_name = schema.get("name")
|
||||
param_location = schema.get("in")
|
||||
param_schema = schema.get("schema")
|
||||
param_typ = param_schema.get("type")
|
||||
dt: PrimitiveType | ADT | None = None
|
||||
if isinstance(param_typ, list) and "null" in param_typ:
|
||||
param_typ.remove("null")
|
||||
if len(param_typ) == 1:
|
||||
param_typ = param_typ[0]
|
||||
if param_typ == "string":
|
||||
# NOTE: this is commented out so far since most of enums are just
|
||||
# too wrong to treat them as enums here
|
||||
# if "enum" in param_schema:
|
||||
# dt = Enum(literals=param_schema["enum"], base_types=[ConstraintString])
|
||||
# else:
|
||||
dt = ConstraintString(**param_schema)
|
||||
elif param_typ == "number":
|
||||
dt = ConstraintNumber(**param_schema)
|
||||
elif param_typ == "integer":
|
||||
dt = ConstraintInteger(**param_schema)
|
||||
elif param_typ == "boolean":
|
||||
dt = PrimitiveBoolean(**param_schema)
|
||||
elif param_typ == "null":
|
||||
dt = PrimitiveNull(**param_schema)
|
||||
elif param_typ == "array":
|
||||
try:
|
||||
items_type = param_schema.get("items").get("type")
|
||||
except Exception:
|
||||
logging.exception("Broken array data: %s", param_schema)
|
||||
raise
|
||||
style = schema.get("style", "form")
|
||||
explode = schema.get("explode", True)
|
||||
if items_type == "string":
|
||||
if style == "form" and not explode:
|
||||
dt = CommaSeparatedList(item_type=ConstraintString())
|
||||
elif style == "form" and explode:
|
||||
dt = Set(item_type=ConstraintString())
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Parameter serialization %s not supported" % schema
|
||||
)
|
||||
|
||||
elif isinstance(param_typ, list):
|
||||
# Param type can be anything. Process supported combinations first
|
||||
if param_location == "query" and param_name == "limit":
|
||||
dt = ConstraintInteger(minimum=0)
|
||||
elif param_location == "query" and sorted(
|
||||
["string", "boolean"]
|
||||
) == sorted(param_typ):
|
||||
dt = PrimitiveBoolean()
|
||||
elif param_location == "query" and sorted(
|
||||
["string", "integer"]
|
||||
) == sorted(param_typ):
|
||||
dt = ConstraintInteger(**param_schema)
|
||||
elif param_location == "query" and sorted(
|
||||
["string", "number"]
|
||||
) == sorted(param_typ):
|
||||
dt = ConstraintNumber(**param_schema)
|
||||
|
||||
if isinstance(dt, ADT):
|
||||
# Set reference into the data_type so that it doesn't mess with main body types
|
||||
dt.reference = Reference(
|
||||
name=param_name, type=RequestParameter, hash_=dicthash_(schema)
|
||||
)
|
||||
|
||||
is_flag: bool = False
|
||||
os_ext = schema.get("x-openstack", {})
|
||||
if not isinstance(os_ext, dict):
|
||||
raise RuntimeError(f"x-openstack must be a dictionary in {schema}")
|
||||
if "is-flag" in os_ext:
|
||||
is_flag = os_ext["is-flag"]
|
||||
|
||||
if dt:
|
||||
return RequestParameter(
|
||||
name=param_name,
|
||||
location=param_location,
|
||||
data_type=dt,
|
||||
description=schema.get("description"),
|
||||
is_required=schema.get("required", False),
|
||||
is_flag=is_flag,
|
||||
)
|
||||
raise NotImplementedError("Parameter %s is not covered yet" % schema)
|
||||
|
||||
raise RuntimeError("Parameter %s is not supported yet" % schema)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,346 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from multiprocessing import Process
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import SpecSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.base import OpenStackServerSourceBase
|
||||
from codegenerator.openapi import cinder_schemas
|
||||
from codegenerator.openapi.utils import merge_api_ref_doc
|
||||
|
||||
|
||||
class CinderV3Generator(OpenStackServerSourceBase):
|
||||
URL_TAG_MAP = {
|
||||
"/versions": "version",
|
||||
}
|
||||
|
||||
def _api_ver_major(self, ver):
|
||||
return ver._ver_major
|
||||
|
||||
def _api_ver_minor(self, ver):
|
||||
return ver._ver_minor
|
||||
|
||||
def _api_ver(self, ver):
|
||||
return (ver._ver_major, ver._ver_minor)
|
||||
|
||||
def generate(self, target_dir, args):
|
||||
proc = Process(target=self._generate, args=[target_dir, args])
|
||||
proc.start()
|
||||
proc.join()
|
||||
if proc.exitcode != 0:
|
||||
raise RuntimeError("Error generating Cinder OpenAPI schma")
|
||||
return Path(target_dir, "openapi_specs", "block-storage", "v3.yaml")
|
||||
|
||||
def _generate(self, target_dir, args, *pargs, **kwargs):
|
||||
from cinder import objects, rpc
|
||||
from cinder.api.openstack import api_version_request
|
||||
from cinder.common import config
|
||||
from cinder.tests.unit.test import Database as db_fixture
|
||||
|
||||
# Register all Cinder objects
|
||||
objects.register_all()
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
self.api_version = api_version_request._MAX_API_VERSION
|
||||
self.min_api_version = api_version_request._MIN_API_VERSION
|
||||
|
||||
rpc.init(CONF)
|
||||
|
||||
CONF.set_default("connection", "sqlite:///", "database")
|
||||
CONF.set_default("sqlite_synchronous", False, "database")
|
||||
|
||||
self.useFixture(db_fixture())
|
||||
|
||||
from cinder.api.v3 import router
|
||||
|
||||
self.router = router.APIRouter()
|
||||
|
||||
work_dir = Path(target_dir)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
impl_path = Path(work_dir, "openapi_specs", "block-storage", "v3.yaml")
|
||||
impl_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
openapi_spec = self.load_openapi(impl_path)
|
||||
if not openapi_spec:
|
||||
openapi_spec = SpecSchema(
|
||||
info=dict(
|
||||
title="OpenStack Volume API",
|
||||
description=LiteralScalarString(
|
||||
"Volume API provided by Cinder service"
|
||||
),
|
||||
version=self.api_version,
|
||||
),
|
||||
openapi="3.1.0",
|
||||
security=[{"ApiKeyAuth": []}],
|
||||
components=dict(
|
||||
securitySchemes={
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Auth-Token",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Set global parameters
|
||||
for name, definition in cinder_schemas.VOLUME_PARAMETERS.items():
|
||||
openapi_spec.components.parameters[name] = ParameterSchema(
|
||||
**definition
|
||||
)
|
||||
|
||||
for route in self.router.map.matchlist:
|
||||
# if route.routepath.startswith("/{project"):
|
||||
# continue
|
||||
if route.routepath.endswith(".:(format)"):
|
||||
continue
|
||||
|
||||
self._process_route(route, openapi_spec, ver_prefix="/v3")
|
||||
|
||||
self._sanitize_param_ver_info(openapi_spec, self.min_api_version)
|
||||
|
||||
if args.api_ref_src:
|
||||
merge_api_ref_doc(openapi_spec, args.api_ref_src)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
|
||||
return impl_path
|
||||
|
||||
def _post_process_operation_hook(
|
||||
self, openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
|
||||
if operationId in [
|
||||
"project_id/volumes:get",
|
||||
"volumes:get",
|
||||
"project_id/volumes/detail:get",
|
||||
"volumes/detail:get",
|
||||
]:
|
||||
for pname in [
|
||||
"all_tenants",
|
||||
"sort",
|
||||
"sort_key",
|
||||
"sort_dir",
|
||||
"limit",
|
||||
"offset",
|
||||
"marker",
|
||||
"with_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"consumes_quota",
|
||||
]:
|
||||
ref = f"#/components/parameters/{pname}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
elif operationId in [
|
||||
"project_id/volumes/summary:get",
|
||||
]:
|
||||
for pname in [
|
||||
"all_tenants",
|
||||
]:
|
||||
ref = f"#/components/parameters/{pname}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
elif operationId in [
|
||||
"project_id/types:get",
|
||||
]:
|
||||
for key, val in cinder_schemas.VOLUME_TYPE_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
def _get_schema_ref(
|
||||
self,
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
):
|
||||
mime_type: str = "application/json"
|
||||
# ### Volume
|
||||
if name == "VolumesListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.VOLUMES_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
if name == "VolumesDetailResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.VOLUMES_DETAIL_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"VolumeShowResponse",
|
||||
"VolumeUpdateResponse",
|
||||
"VolumesCreateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.VOLUME_CONTAINER_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# ### Volume Metadata
|
||||
elif name in [
|
||||
"VolumesMetadataListResponse",
|
||||
"VolumesMetadataUpdate_All",
|
||||
"VolumesMetadataUpdate_AllResponse",
|
||||
"VolumesMetadataCreateResponse",
|
||||
"VolumesActionOs-Set_Image_MetadataResponse",
|
||||
"VolumesActionOs-Show_Image_MetadataResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.METADATA_CONTAINER_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"VolumesMetadataShowResponse",
|
||||
"VolumesMetadataUpdate",
|
||||
"VolumesMetadataUpdateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.METADATA_ITEM_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# Volume Actions
|
||||
elif name == "VolumesActionRevertResponse":
|
||||
return (None, None)
|
||||
elif name == "VolumesActionOs-Reset_StatusRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**cinder_schemas.VOLUME_RESET_STATUS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"VolumesActionOs-Reset_StatusResponse",
|
||||
"VolumesActionOs-Force_DeleteResponse",
|
||||
"VolumesActionOs-Force_DetachResponse",
|
||||
"VolumesActionOs-Migrate_VolumeResponse",
|
||||
"VolumesActionOs-Migrate_Volume_CompletionResponse",
|
||||
"VolumesActionOs-AttachResponse",
|
||||
"VolumesActionOs-DetachResponse",
|
||||
"VolumesActionOs-ReserveResponse",
|
||||
"VolumesActionOs-UnreserveResponse",
|
||||
"VolumesActionOs-Begin_DetachingResponse",
|
||||
"VolumesActionOs-Roll_DetachingResponse",
|
||||
"VolumesActionOs-Initialize_ConnectionResponse",
|
||||
"VolumesActionOs-Terminate_ConnectionResponse",
|
||||
"VolumesActionOs-ExtendResponse",
|
||||
"VolumesActionOs-Update_Readonly_FlagResponse",
|
||||
"VolumesActionOs-RetypeResponse",
|
||||
"VolumesActionOs-Set_BootableResponse",
|
||||
"VolumesActionOs-ReimageResponse",
|
||||
"VolumesActionOs-Unset_Image_MetadataResponse",
|
||||
"VolumesActionOs-UnmanageResponse",
|
||||
]:
|
||||
return (None, None)
|
||||
elif name == "VolumesActionOs-Volume_Upload_ImageResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**cinder_schemas.VOLUME_UPLOAD_IMAGE_RESPONSE_SCHEMA
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# ### Volume Type
|
||||
elif name == "TypesListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPES_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"TypesCreateResponse",
|
||||
"TypeShowResponse",
|
||||
"TypeUpdateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPE_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"TypesExtra_SpecsListResponse",
|
||||
"TypesExtra_SpecsCreateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPE_EXTRA_SPECS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
elif name in [
|
||||
"TypesExtra_SpecShowResponse",
|
||||
"TypesExtra_SpecUpdateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPE_EXTRA_SPEC_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
elif name == "TypesOs_Volume_Type_AccessListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPE_ACCESS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"TypesActionAddprojectaccessResponse",
|
||||
"TypesActionRemoveprojectaccessResponse",
|
||||
]:
|
||||
return (None, None)
|
||||
|
||||
# ### Volume Type Encryption
|
||||
# this is not really a list operation, but who cares
|
||||
elif name == "TypesEncryptionListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**cinder_schemas.VOLUME_TYPE_ENCRYPTION_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "TypesEncryptionShowResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**cinder_schemas.VOLUME_TYPE_ENCRYPTION_SHOW_SCHEMA
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"TypesEncryptionCreateResponse",
|
||||
"TypesEncryptionUpdateResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**cinder_schemas.VOLUME_TYPE_ENCRYPTION_CONTAINER_SCHEMA
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
# Default
|
||||
else:
|
||||
(ref, mime_type) = super()._get_schema_ref(
|
||||
openapi_spec, name, description, action_name=action_name
|
||||
)
|
||||
return (ref, mime_type)
|
||||
@@ -0,0 +1,609 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
from typing import Any
|
||||
|
||||
from cinder.api.schemas import admin_actions
|
||||
from cinder.api.validation import parameter_types
|
||||
|
||||
# NOTE(gtema): This is a temporary location for schemas not currently defined
|
||||
# in Glance. Once everything is stabilized those must be moved directly to Glabne
|
||||
|
||||
LINK_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "Links to the resources in question. See [API Guide / Links and References](https://docs.openstack.org/api-guide/compute/links_and_references.html) for more info.",
|
||||
"properties": {
|
||||
"href": {"type": "string", "format": "uri"},
|
||||
"rel": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
LINKS_SCHEMA: dict[str, Any] = {
|
||||
"type": "array",
|
||||
"description": "Links to the resources in question. See [API Guide / Links and References](https://docs.openstack.org/api-guide/compute/links_and_references.html) for more info.",
|
||||
"items": copy.deepcopy(LINK_SCHEMA),
|
||||
}
|
||||
|
||||
ATTACHMENT_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {"type": "string", "format": "uuid"},
|
||||
"attachment_id": {"type": "string", "format": "uuid"},
|
||||
"attached_at": {"type": "string", "format": "date-time"},
|
||||
"host_name": {"type": "string"},
|
||||
"volume_id": {"type": "string", "format": "uuid"},
|
||||
"device": {"type": "string"},
|
||||
"id": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
ATTACHMENTS_SCHEMA = {
|
||||
"type": "array",
|
||||
"items": copy.deepcopy(ATTACHMENT_SCHEMA),
|
||||
}
|
||||
|
||||
METADATA_SCHEMA = {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9-_:. /]{1,255}$": {"type": "string", "maxLength": 255},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"description": "A metadata object. Contains one or more metadata key and value pairs that are associated with the resource.",
|
||||
}
|
||||
|
||||
METADATA_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "Metadata key and value pairs. The maximum size for each metadata key and value pair is 255 bytes.",
|
||||
"properties": {"metadata": METADATA_SCHEMA},
|
||||
}
|
||||
|
||||
METADATA_ITEM_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "Metadata key and value pairs. The maximum size for each metadata key and value pair is 255 bytes.",
|
||||
"properties": {"meta": {"maxProperties": 1, **METADATA_SCHEMA}},
|
||||
}
|
||||
|
||||
VOLUME_SHORT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A volume object.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The volume name.",
|
||||
},
|
||||
"links": {
|
||||
"description": "The volume links.",
|
||||
**copy.deepcopy(LINKS_SCHEMA),
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the volume.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A volume object.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The volume name.",
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The volume description.",
|
||||
},
|
||||
"volume_type": {
|
||||
"type": "string",
|
||||
"description": "The associated volume type name for the volume.",
|
||||
},
|
||||
"metadata": copy.deepcopy(METADATA_SCHEMA),
|
||||
"snapshot_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "To create a volume from an existing snapshot, specify the UUID of the volume snapshot. The volume is created in same availability zone and with same size as the snapshot.",
|
||||
},
|
||||
"source_volid": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the source volume. The API creates a new volume with the same size as the source volume unless a larger size is requested.",
|
||||
},
|
||||
"consistencygroup_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the consistency group.",
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The size of the volume, in gibibytes (GiB).",
|
||||
},
|
||||
"availability_zone": {
|
||||
"type": "string",
|
||||
"description": "The name of the availability zone.",
|
||||
},
|
||||
"multiattach": {
|
||||
"type": "boolean",
|
||||
"description": "If true, this volume can attach to more than one instance.",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The volume status.",
|
||||
},
|
||||
"migration_status": {
|
||||
"type": "string",
|
||||
"description": "The volume migration status. Admin only.",
|
||||
},
|
||||
"attachments": {
|
||||
"description": "Instance attachment information. If this volume is attached to a server instance, the attachments list includes the UUID of the attached server, an attachment UUID, the name of the attached host, if any, the volume UUID, the device, and the device UUID. Otherwise, this list is empty.",
|
||||
**copy.deepcopy(ATTACHMENTS_SCHEMA),
|
||||
},
|
||||
"links": {
|
||||
"description": "The volume links.",
|
||||
**copy.deepcopy(LINKS_SCHEMA),
|
||||
},
|
||||
"encrypted": {
|
||||
"type": "boolean",
|
||||
"description": "If true, this volume is encrypted.",
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was created.",
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was updated.",
|
||||
},
|
||||
"replication_status": {
|
||||
"type": "string",
|
||||
"description": "The volume replication status.",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the volume.",
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the user.",
|
||||
},
|
||||
"volume_type_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The associated volume type ID for the volume.",
|
||||
"x-openstack": {"min-ver": "3.63"},
|
||||
},
|
||||
"group_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the group.",
|
||||
"x-openstack": {"min-ver": "3.63"},
|
||||
},
|
||||
"provider_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "The provider ID for the volume. The value is either a string set by the driver or null if the driver doesn’t use the field or if it hasn’t created it yet. Only returned for administrators.",
|
||||
"x-openstack": {"min-ver": "3.21"},
|
||||
},
|
||||
"service_uuid": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A unique identifier that’s used to indicate what node the volume-service for a particular volume is being serviced by.",
|
||||
"x-openstack": {"min-ver": "3.48"},
|
||||
},
|
||||
"shared_targets": {
|
||||
"type": "boolean",
|
||||
"description": "An indicator whether the host connecting the volume should lock for the whole attach/detach process or not. true means only is iSCSI initiator running on host doesn’t support manual scans, false means never use locks, and null means to always use locks. Look at os-brick’s guard_connection context manager. Default=True.",
|
||||
"x-openstack": {"min-ver": "3.48"},
|
||||
},
|
||||
"cluster_name": {
|
||||
"type": "string",
|
||||
"description": "The cluster name of volume backend.",
|
||||
"x-openstack": {"min-ver": "3.61"},
|
||||
},
|
||||
"consumes_quota": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this resource consumes quota or not. Resources that not counted for quota usage are usually temporary internal resources created to perform an operation.",
|
||||
"x-openstack": {"min-ver": "3.65"},
|
||||
},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
VOLUME_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A volume object.",
|
||||
"properties": {"volume": copy.deepcopy(VOLUME_SCHEMA)},
|
||||
"required": ["volume"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
VOLUMES_SCHEMA = {
|
||||
"type": "object",
|
||||
"description": "A container with list of volume objects.",
|
||||
"properties": {
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": copy.deepcopy(VOLUME_SHORT_SCHEMA),
|
||||
"description": "A list of volume objects.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUMES_DETAIL_SCHEMA = {
|
||||
"type": "object",
|
||||
"description": "A container with list of volume objects.",
|
||||
"properties": {
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": copy.deepcopy(VOLUME_SCHEMA),
|
||||
"description": "A list of volume objects.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_PARAMETERS = {
|
||||
"all_tenants": {
|
||||
"in": "query",
|
||||
"name": "all_tenans",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"description": "Shows details for all project. Admin only.",
|
||||
},
|
||||
"sort": {
|
||||
"in": "query",
|
||||
"name": "sort",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
"description": "Comma-separated list of sort keys and optional sort directions in the form of < key > [: < direction > ]. A valid direction is asc (ascending) or desc (descending).",
|
||||
},
|
||||
"sort_key": {
|
||||
"in": "query",
|
||||
"name": "sort_key",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
"description": "Sorts by an attribute. A valid value is name, status, container_format, disk_format, size, id, created_at, or updated_at. Default is created_at. The API uses the natural sorting direction of the sort_key attribute value. Deprecated in favour of the combined sort parameter.",
|
||||
},
|
||||
"sort_dir": {
|
||||
"in": "query",
|
||||
"name": "sort_dir",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["asc", "desc"],
|
||||
},
|
||||
"description": "Sorts by one or more sets of attribute and sort direction combinations. If you omit the sort direction in a set, default is desc. Deprecated in favour of the combined sort parameter.",
|
||||
},
|
||||
"limit": {
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
},
|
||||
"description": "Requests a page size of items. Returns a number of items up to a limit value. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request.",
|
||||
},
|
||||
"offset": {
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
},
|
||||
"description": "Used in conjunction with limit to return a slice of items. offset is where to start in the list.",
|
||||
},
|
||||
"marker": {
|
||||
"in": "query",
|
||||
"name": "marker",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
"description": "The ID of the last-seen item. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request.",
|
||||
},
|
||||
"with_count": {
|
||||
"in": "query",
|
||||
"name": "with_count",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"description": "Whether to show count in API response or not, default is False.",
|
||||
"x-openstack": {"min-ver": "3.45"},
|
||||
},
|
||||
"created_at": {
|
||||
"in": "query",
|
||||
"name": "created_at",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"description": "Filters reuslts by a time that resources are created at with time comparison operators: gt/gte/eq/neq/lt/lte.",
|
||||
"x-openstack": {"min-ver": "3.60"},
|
||||
},
|
||||
"updated_at": {
|
||||
"in": "query",
|
||||
"name": "updated_at",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"description": "Filters reuslts by a time that resources are updated at with time comparison operators: gt/gte/eq/neq/lt/lte.",
|
||||
"x-openstack": {"min-ver": "3.60"},
|
||||
},
|
||||
"consumes_quota": {
|
||||
"in": "query",
|
||||
"name": "consumes_quota",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"description": "Filters results by consumes_quota field. Resources that don’t use quotas are usually temporary internal resources created to perform an operation. Default is to not filter by it. Filtering by this option may not be always possible in a cloud, see List Resource Filters to determine whether this filter is available in your cloud.",
|
||||
"x-openstack": {"min-ver": "3.65"},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_RESET_STATUS_SCHEMA: dict[str, Any] = admin_actions.reset
|
||||
|
||||
VOLUME_UPLOAD_IMAGE_RESPONSE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"container_format": {
|
||||
"type": "string",
|
||||
"description": "Container format for the new image. Default is bare.",
|
||||
},
|
||||
"disk_format": {
|
||||
"type": "string",
|
||||
"description": "Disk format for the new image. Default is raw.",
|
||||
},
|
||||
"display_description": {
|
||||
"type": "string",
|
||||
"description": "The volume description.",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the volume.",
|
||||
},
|
||||
"image_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The uuid for the new image.",
|
||||
},
|
||||
"image_name": {
|
||||
"type": "string",
|
||||
"description": "The name for the new image.",
|
||||
},
|
||||
"protected": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the new image is protected. Default=False.",
|
||||
"x-openstack": {"min-ver": "3.1"},
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The size of the volume, in gibibytes (GiB).",
|
||||
},
|
||||
"status": {"type": "integer", "description": "The volume status."},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was updated.",
|
||||
},
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"description": "The visibility property of the new image. Default is private.",
|
||||
"x-openstack": {"min-ver": "3.1"},
|
||||
},
|
||||
"volume_type": {
|
||||
"type": "string",
|
||||
"description": "The associated volume type name for the volume.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The volume type description.",
|
||||
},
|
||||
"extra_specs": {
|
||||
"description": "A key and value pair that contains additional specifications that are associated with the volume type. Examples include capabilities, capacity, compression, and so on, depending on the storage driver in use.",
|
||||
**parameter_types.extra_specs_with_no_spaces_key,
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the volume type.",
|
||||
},
|
||||
"is_public": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the volume type is publicly visible.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The volume type description.",
|
||||
},
|
||||
"os-volume-type-access:is_public": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the volume type is publicly visible.",
|
||||
},
|
||||
"qos_specs_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The QoS specifications ID.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"volume_type": VOLUME_TYPE_SCHEMA},
|
||||
}
|
||||
|
||||
VOLUME_TYPES_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"volume_types": {"type": "array", "items": VOLUME_TYPE_SCHEMA}
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_LIST_PARAMETERS: dict[str, Any] = {
|
||||
"type_is_public": {
|
||||
"in": "query",
|
||||
"name": "is_public",
|
||||
"description": "Filter the volume type by public visibility.",
|
||||
"schema": {"type": "boolean"},
|
||||
},
|
||||
"type_sort": {
|
||||
"in": "query",
|
||||
"name": "sort",
|
||||
"description": "Comma-separated list of sort keys and optional sort directions in the form of < key > [: < direction > ]. A valid direction is asc (ascending) or desc (descending).",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"type_sort_key": {
|
||||
"in": "query",
|
||||
"name": "sort_key",
|
||||
"description": "Sorts by an attribute. A valid value is name, status, container_format, disk_format, size, id, created_at, or updated_at. Default is created_at. The API uses the natural sorting direction of the sort_key attribute value. Deprecated in favour of the combined sort parameter.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"type_sort_dir": {
|
||||
"in": "query",
|
||||
"name": "sort_dir",
|
||||
"description": "Sorts by one or more sets of attribute and sort direction combinations. If you omit the sort direction in a set, default is desc. Deprecated in favour of the combined sort parameter.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"type_limit": {
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"description": "Requests a page size of items. Returns a number of items up to a limit value. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request.",
|
||||
"schema": {"type": "integer"},
|
||||
},
|
||||
"type_marker": {
|
||||
"in": "query",
|
||||
"name": "marker",
|
||||
"description": "The ID of the last-seen item. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"type_offset": {
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"description": "Used in conjunction with limit to return a slice of items. offset is where to start in the list.",
|
||||
"schema": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_EXTRA_SPECS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extra_specs": {
|
||||
"description": "A key and value pair that contains additional specifications that are associated with the volume type. Examples include capabilities, capacity, compression, and so on, depending on the storage driver in use.",
|
||||
**parameter_types.extra_specs_with_no_spaces_key,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_EXTRA_SPEC_SCHEMA: dict[str, Any] = (
|
||||
parameter_types.extra_specs_with_no_spaces_key
|
||||
)
|
||||
|
||||
VOLUME_TYPE_ACCESS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"volume_type_access": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the project.",
|
||||
},
|
||||
"volume_type_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the volume type.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_ENCRYPTION_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cipher": {
|
||||
"type": "string",
|
||||
"description": "The encryption algorithm or mode. For example, aes-xts-plain64. The default value is None.",
|
||||
},
|
||||
"control_location": {
|
||||
"type": "string",
|
||||
"enum": ["front-end", "back-end"],
|
||||
"description": "Notional service where encryption is performed. Valid values are “front-end” or “back-end”. The default value is “front-end”.",
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was created.",
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
"description": "The resource is deleted or not.",
|
||||
},
|
||||
"deleted_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was deleted.",
|
||||
},
|
||||
"encryption_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the encryption.",
|
||||
},
|
||||
"key_size": {
|
||||
"type": "integer",
|
||||
"description": "Size of encryption key, in bits. This is usually 256. The default value is None.",
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"description": "The class that provides encryption support.",
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the resource was updated.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_ENCRYPTION_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"encryption": VOLUME_TYPE_ENCRYPTION_SCHEMA},
|
||||
}
|
||||
|
||||
VOLUME_TYPE_ENCRYPTION_SHOW_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cipher": {
|
||||
"type": "string",
|
||||
"description": "The encryption algorithm or mode. For example, aes-xts-plain64. The default value is None.",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
from multiprocessing import Process
|
||||
from pathlib import Path
|
||||
|
||||
from jsonref import replace_refs
|
||||
import routes
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from codegenerator.common.schema import (
|
||||
SpecSchema,
|
||||
TypeSchema,
|
||||
ParameterSchema,
|
||||
HeaderSchema,
|
||||
)
|
||||
from codegenerator.openapi.base import OpenStackServerSourceBase
|
||||
from codegenerator.openapi.utils import merge_api_ref_doc
|
||||
|
||||
IMAGE_PARAMETERS = {
|
||||
"limit": {
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"description": LiteralScalarString(
|
||||
"Requests a page size of items. Returns a number of items up to a limit value. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request."
|
||||
),
|
||||
"schema": {"type": "integer"},
|
||||
},
|
||||
"marker": {
|
||||
"in": "query",
|
||||
"name": "marker",
|
||||
"description": LiteralScalarString(
|
||||
"The ID of the last-seen item. Use the limit parameter to make an initial limited request and use the ID of the last-seen item from the response as the marker parameter value in a subsequent limited request."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"id": {
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "id filter parameter",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"name": {
|
||||
"in": "query",
|
||||
"name": "name",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by a name, as a string. A valid value is the name of an image."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"visibility": {
|
||||
"in": "query",
|
||||
"name": "visibility",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by an image visibility value. A valid value is public, private, community, shared, or all. (Note that if you filter on shared, the images included in the response will only be those where your member status is accepted unless you explicitly include a member_status filter in the request.) If you omit this parameter, the response shows public, private, and those shared images with a member status of accepted."
|
||||
),
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["public", "private", "community", "shared", "all"],
|
||||
},
|
||||
},
|
||||
"member_status": {
|
||||
"in": "query",
|
||||
"name": "member_status",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by a member status. A valid value is accepted, pending, rejected, or all. Default is accepted."
|
||||
),
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["accepted", "pending", "rejected", "all"],
|
||||
},
|
||||
},
|
||||
"owner": {
|
||||
"in": "query",
|
||||
"name": "owner",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by a project (also called a “tenant”) ID. Shows only images that are shared with you by the specified owner."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"status": {
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by an image status."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"size_min": {
|
||||
"in": "query",
|
||||
"name": "size_min",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by a minimum image size, in bytes."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"size_max": {
|
||||
"in": "query",
|
||||
"name": "size_max",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by a maximum image size, in bytes."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"protected": {
|
||||
"in": "query",
|
||||
"name": "protected",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by the ‘protected’ image property. A valid value is one of ‘true’, ‘false’ (must be all lowercase). Any other value will result in a 400 response."
|
||||
),
|
||||
"schema": {"type": "boolean"},
|
||||
},
|
||||
"os_hidden": {
|
||||
"in": "query",
|
||||
"name": "os_hidden",
|
||||
"description": LiteralScalarString(
|
||||
'When true, filters the response to display only "hidden" images. By default, "hidden" images are not included in the image-list response. (Since Image API v2.7)'
|
||||
),
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.7"},
|
||||
},
|
||||
"sort_key": {
|
||||
"in": "query",
|
||||
"name": "sort_key",
|
||||
"description": LiteralScalarString(
|
||||
"Sorts the response by an attribute, such as name, id, or updated_at. Default is created_at. The API uses the natural sorting direction of the sort_key image attribute."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"sort_dir": {
|
||||
"in": "query",
|
||||
"name": "sort_dir",
|
||||
"description": LiteralScalarString(
|
||||
"Sorts the response by a set of one or more sort direction and attribute (sort_key) combinations. A valid value for the sort direction is asc (ascending) or desc (descending). If you omit the sort direction in a set, the default is desc."
|
||||
),
|
||||
"schema": {"type": "string", "enum": ["asc", "desc"]},
|
||||
},
|
||||
"sort": {
|
||||
"in": "query",
|
||||
"name": "sort",
|
||||
"description": LiteralScalarString(
|
||||
"Sorts the response by one or more attribute and sort direction combinations. You can also set multiple sort keys and directions. Default direction is desc. Use the comma (,) character to separate multiple values. For example: `sort=name:asc,status:desc`"
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"tag": {
|
||||
"in": "query",
|
||||
"name": "tag",
|
||||
"description": LiteralScalarString(
|
||||
"Filters the response by the specified tag value. May be repeated, but keep in mind that you're making a conjunctive query, so only images containing all the tags specified will appear in the response."
|
||||
),
|
||||
"schema": {"type": "array", "items": {"type": "string"}},
|
||||
"style": "form",
|
||||
"explode": True,
|
||||
},
|
||||
"created_at": {
|
||||
"in": "query",
|
||||
"name": "created_at",
|
||||
"description": LiteralScalarString(
|
||||
"Specify a comparison filter based on the date and time when the resource was created."
|
||||
),
|
||||
"schema": {"type": "string", "format": "date-time"},
|
||||
},
|
||||
"updated_at": {
|
||||
"in": "query",
|
||||
"name": "updated_at",
|
||||
"description": LiteralScalarString(
|
||||
"Specify a comparison filter based on the date and time when the resource was most recently modified."
|
||||
),
|
||||
"schema": {"type": "string", "format": "date-time"},
|
||||
},
|
||||
"range": {
|
||||
"in": "header",
|
||||
"name": "Range",
|
||||
"description": LiteralScalarString(
|
||||
"The range of image data requested. Note that multi range requests are not supported."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"content-type": {
|
||||
"in": "header",
|
||||
"name": "Content-Type",
|
||||
"description": LiteralScalarString(
|
||||
"The media type descriptor of the body, namely application/octet-stream"
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"x-image-meta-store": {
|
||||
"in": "header",
|
||||
"name": "X-Image-Meta-Store",
|
||||
"description": LiteralScalarString(
|
||||
"A store identifier to upload or import image data. Should only be included when making a request to a cloud that supports multiple backing stores. Use the Store Discovery call to determine an appropriate store identifier. Simply omit this header to use the default store."
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
IMAGE_HEADERS = {
|
||||
"Content-Type": {
|
||||
"description": LiteralScalarString(
|
||||
"The media type descriptor of the body, namely application/octet-stream"
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"Content-Length": {
|
||||
"description": LiteralScalarString(
|
||||
"The length of the body in octets (8-bit bytes)"
|
||||
),
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"Content-Md5": {
|
||||
"description": "The MD5 checksum of the body",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"Content-Range": {
|
||||
"description": "The content range of image data",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"OpenStack-image-store-ids": {
|
||||
"description": "list of available stores",
|
||||
"schema": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class GlanceGenerator(OpenStackServerSourceBase):
|
||||
URL_TAG_MAP = {
|
||||
"/versions": "version",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.api_version = "2.16"
|
||||
self.min_api_version = None
|
||||
|
||||
def _api_ver_major(self, ver):
|
||||
return ver.ver_major
|
||||
|
||||
def _api_ver_minor(self, ver):
|
||||
return ver.ver_minor
|
||||
|
||||
def _api_ver(self, ver):
|
||||
return (ver.ver_major, ver.ver_minor)
|
||||
|
||||
def generate(self, target_dir, args):
|
||||
proc = Process(target=self._generate, args=[target_dir, args])
|
||||
proc.start()
|
||||
proc.join()
|
||||
if proc.exitcode != 0:
|
||||
raise RuntimeError("Error generating Glance OpenAPI schma")
|
||||
return Path(target_dir, "openapi_specs", "image", "v2.yaml")
|
||||
|
||||
def _generate(self, target_dir, args):
|
||||
from glance.api.v2 import router
|
||||
from glance.common import config
|
||||
from oslo_config import fixture as cfg_fixture
|
||||
|
||||
self._config_fixture = self.useFixture(cfg_fixture.Config())
|
||||
|
||||
config.parse_args(args=[])
|
||||
|
||||
self.router = router.API(routes.Mapper())
|
||||
|
||||
work_dir = Path(target_dir)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
impl_path = Path(work_dir, "openapi_specs", "image", "v2.yaml")
|
||||
impl_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
openapi_spec = self.load_openapi(impl_path)
|
||||
if not openapi_spec:
|
||||
openapi_spec = SpecSchema(
|
||||
info=dict(
|
||||
title="OpenStack Image API",
|
||||
description=LiteralScalarString(
|
||||
"Image API provided by Glance service"
|
||||
),
|
||||
version=self.api_version,
|
||||
),
|
||||
openapi="3.1.0",
|
||||
security=[{"ApiKeyAuth": []}],
|
||||
components=dict(
|
||||
securitySchemes={
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Auth-Token",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Set global headers and parameters
|
||||
for name, definition in IMAGE_PARAMETERS.items():
|
||||
openapi_spec.components.parameters[name] = ParameterSchema(
|
||||
**definition
|
||||
)
|
||||
for name, definition in IMAGE_HEADERS.items():
|
||||
openapi_spec.components.headers[name] = HeaderSchema(**definition)
|
||||
|
||||
for route in self.router.map.matchlist:
|
||||
if not route.conditions:
|
||||
continue
|
||||
self._process_route(route, openapi_spec, ver_prefix="/v2")
|
||||
|
||||
self._sanitize_param_ver_info(openapi_spec, self.min_api_version)
|
||||
|
||||
if args.api_ref_src:
|
||||
merge_api_ref_doc(openapi_spec, args.api_ref_src)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
|
||||
return impl_path
|
||||
|
||||
def _post_process_operation_hook(
|
||||
self, openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
|
||||
if operationId == "images:get":
|
||||
for pname in [
|
||||
"limit",
|
||||
"marker",
|
||||
"name",
|
||||
"id",
|
||||
"owner",
|
||||
"protected",
|
||||
"status",
|
||||
"tag",
|
||||
"visibility",
|
||||
"os_hidden",
|
||||
"member_status",
|
||||
"size_max",
|
||||
"size_min",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"sort_dir",
|
||||
"sort_key",
|
||||
"sort",
|
||||
]:
|
||||
ref = f"#/components/parameters/{pname}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
elif operationId == "images:post":
|
||||
key = "OpenStack-image-store-ids"
|
||||
ref = f"#/components/headers/{key}"
|
||||
operation_spec.responses["201"].setdefault("headers", {})
|
||||
operation_spec.responses["201"]["headers"].update(
|
||||
{key: {"$ref": ref}}
|
||||
)
|
||||
|
||||
elif operationId == "images/image_id/file:put":
|
||||
for ref in [
|
||||
"#/components/parameters/content-type",
|
||||
"#/components/parameters/x-image-meta-store",
|
||||
]:
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
elif operationId == "images/image_id/file:get":
|
||||
for ref in [
|
||||
"#/components/parameters/range",
|
||||
]:
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
for code in ["200", "206"]:
|
||||
operation_spec.responses[code].setdefault("headers", {})
|
||||
for hdr in ["Content-Type", "Content-Md5", "Content-Length"]:
|
||||
operation_spec.responses[code]["headers"].setdefault(
|
||||
hdr,
|
||||
{"$ref": f"#/components/headers/{hdr}"},
|
||||
)
|
||||
operation_spec.responses["206"]["headers"].setdefault(
|
||||
"Content-Range",
|
||||
{"$ref": "#/components/headers/Content-Range"},
|
||||
)
|
||||
|
||||
def _get_schema_ref(
|
||||
self,
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
):
|
||||
from glance.api.v2 import image_members
|
||||
from glance.api.v2 import images
|
||||
from glance.api.v2 import metadef_namespaces
|
||||
from glance.api.v2 import metadef_objects
|
||||
from glance.api.v2 import metadef_properties
|
||||
from glance.api.v2 import metadef_resource_types
|
||||
from glance.api.v2 import metadef_tags
|
||||
from glance.api.v2 import tasks
|
||||
from glance import schema as glance_schema
|
||||
|
||||
ref: str
|
||||
mime_type: str = "application/json"
|
||||
|
||||
if name == "TasksListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**{
|
||||
"name": "tasks",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {"type": "string"},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": copy.deepcopy(
|
||||
schema_def.properties
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name.startswith("Schemas") and name.endswith("Response"):
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(type="string", description="Schema data as string"),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImagesTasksGet_Task_InfoResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
glance_schema.CollectionSchema(
|
||||
"tasks", tasks.get_task_schema()
|
||||
),
|
||||
name,
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImagesImportImport_ImageRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"uri": {"type": "string"},
|
||||
"glance_image_id": {"type": "string"},
|
||||
"glance_region": {"type": "string"},
|
||||
"glance_service_interface": {
|
||||
"type": "string"
|
||||
},
|
||||
},
|
||||
},
|
||||
"stores": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"all_stores": {"type": "boolean"},
|
||||
"all_stores_must_success": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImagesImportImport_ImageResponse":
|
||||
openapi_spec.components.schemas.setdefault(name, TypeSchema())
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImagesListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(images.get_collection_schema(), name),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImagesMembersListResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
image_members.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"InfoImportGet_Image_ImportResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"import-methods": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"InfoStoresGet_StoresResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"default": {"type": "boolean"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"InfoStoresDetailGet_Stores_DetailResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
**{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stores": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"default": {"type": "boolean"},
|
||||
"type": {"type": "string"},
|
||||
"weight": {"type": "number"},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"MetadefsNamespacesListResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
metadef_namespaces.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"MetadefsNamespacesObjectsListResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
metadef_objects.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"MetadefsNamespacesPropertiesListResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
metadef_properties.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"MetadefsResource_TypesListResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
metadef_resource_types.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"MetadefsNamespacesTagsListResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
self._get_glance_schema(
|
||||
metadef_tags.get_collection_schema(), name
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ImageUpdateRequest":
|
||||
# openapi_spec.components.schemas.setdefault(
|
||||
# name,
|
||||
# self._get_glance_schema(
|
||||
# metadef_tags.get_collection_schema(), name
|
||||
# ),
|
||||
# )
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**{"type": "string", "format": "RFC 6902"}),
|
||||
)
|
||||
mime_type = "application/openstack-images-v2.1-json-patch"
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"ImagesFileUploadRequest",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**{"type": "string", "format": "binary"}),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
mime_type = "application/octet-stream"
|
||||
elif name in [
|
||||
"ImagesFileDownloadResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**{"type": "string", "format": "binary"}),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
mime_type = "application/octet-stream"
|
||||
elif name in [
|
||||
"ImagesFileUploadResponse",
|
||||
"ImagesFileDownloadResponse",
|
||||
]:
|
||||
return (None, None)
|
||||
elif schema_def:
|
||||
# Schema is known and is not an exception
|
||||
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, self._get_glance_schema(schema_def, name)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
else:
|
||||
(ref, mime_type) = super()._get_schema_ref(
|
||||
openapi_spec, name, description, schema_def=schema_def
|
||||
)
|
||||
return (ref, mime_type)
|
||||
|
||||
def _get_glance_schema(self, schema, name: str | None = None):
|
||||
res = replace_refs(schema.raw(), proxies=False)
|
||||
res.pop("definitions", None)
|
||||
if "properties" in res and "type" not in res:
|
||||
res["type"] = "object"
|
||||
# List of image props that are by default integer, but in real life
|
||||
# are surely going i64 side
|
||||
i32_fixes = ["size", "virtual_size"]
|
||||
if name and name == "ImagesListResponse":
|
||||
for field in i32_fixes:
|
||||
res["properties"]["images"]["items"]["properties"][field][
|
||||
"format"
|
||||
] = "int64"
|
||||
if name and name == "ImageShowResponse":
|
||||
for field in i32_fixes:
|
||||
res["properties"][field]["format"] = "int64"
|
||||
return TypeSchema(**res)
|
||||
|
||||
@classmethod
|
||||
def _get_response_codes(cls, method: str, operationId: str) -> list[str]:
|
||||
response_codes = super()._get_response_codes(method, operationId)
|
||||
if operationId == "images/image_id/file:put":
|
||||
response_codes = ["204"]
|
||||
if operationId == "images/image_id/file:get":
|
||||
response_codes = ["200", "204", "206"]
|
||||
return response_codes
|
||||
@@ -0,0 +1,483 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import inspect
|
||||
from multiprocessing import Process
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import PathSchema
|
||||
from codegenerator.common.schema import SpecSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.base import OpenStackServerSourceBase
|
||||
from codegenerator.openapi.keystone_schemas import application_credential
|
||||
from codegenerator.openapi.keystone_schemas import auth
|
||||
from codegenerator.openapi.keystone_schemas import common
|
||||
from codegenerator.openapi.keystone_schemas import domain
|
||||
from codegenerator.openapi.keystone_schemas import endpoint
|
||||
from codegenerator.openapi.keystone_schemas import federation
|
||||
from codegenerator.openapi.keystone_schemas import group
|
||||
from codegenerator.openapi.keystone_schemas import project
|
||||
from codegenerator.openapi.keystone_schemas import region
|
||||
from codegenerator.openapi.keystone_schemas import role
|
||||
from codegenerator.openapi.keystone_schemas import service
|
||||
from codegenerator.openapi.keystone_schemas import user
|
||||
from codegenerator.openapi.utils import merge_api_ref_doc
|
||||
|
||||
|
||||
class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
URL_TAG_MAP = {
|
||||
"/versions": "version",
|
||||
}
|
||||
|
||||
RESOURCE_MODULES = [
|
||||
application_credential,
|
||||
auth,
|
||||
common,
|
||||
domain,
|
||||
endpoint,
|
||||
federation,
|
||||
group,
|
||||
project,
|
||||
region,
|
||||
role,
|
||||
service,
|
||||
user,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.api_version = "3.0"
|
||||
self.min_api_version = "3.14"
|
||||
|
||||
def _api_ver_major(self, ver):
|
||||
return ver._ver_major
|
||||
|
||||
def _api_ver_minor(self, ver):
|
||||
return ver._ver_minor
|
||||
|
||||
def _api_ver(self, ver):
|
||||
return (ver._ver_major, ver._ver_minor)
|
||||
|
||||
def generate(self, target_dir, args):
|
||||
proc = Process(target=self._generate, args=[target_dir, args])
|
||||
proc.start()
|
||||
proc.join()
|
||||
if proc.exitcode != 0:
|
||||
raise RuntimeError("Error generating Keystone OpenAPI schema")
|
||||
return Path(target_dir, "openapi_specs", "identity", "v3.yaml")
|
||||
|
||||
def _generate(self, target_dir, args, *pargs, **kwargs):
|
||||
from keystone.server.flask import application
|
||||
|
||||
self.app = application.application_factory()
|
||||
self.router = self.app.url_map
|
||||
|
||||
work_dir = Path(target_dir)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
impl_path = Path(work_dir, "openapi_specs", "identity", "v3.yaml")
|
||||
impl_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
openapi_spec = self.load_openapi(impl_path)
|
||||
if not openapi_spec:
|
||||
openapi_spec = SpecSchema(
|
||||
info=dict(
|
||||
title="OpenStack Identity API",
|
||||
description=LiteralScalarString(
|
||||
"Identity API provided by Keystone service"
|
||||
),
|
||||
version=self.api_version,
|
||||
),
|
||||
openapi="3.1.0",
|
||||
security=[{"ApiKeyAuth": []}],
|
||||
components=dict(
|
||||
securitySchemes={
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Auth-Token",
|
||||
}
|
||||
},
|
||||
headers={
|
||||
"X-Auth-Token": {
|
||||
"description": "A valid authentication token",
|
||||
"schema": {"type": "string", "format": "secret"},
|
||||
},
|
||||
"X-Subject-Token": {
|
||||
"description": "A valid authentication token",
|
||||
"schema": {"type": "string", "format": "secret"},
|
||||
},
|
||||
"Openstack-Auth-Receipt": {
|
||||
"description": "The auth receipt. A partially successful authentication response returns the auth receipt ID in this header rather than in the response body.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
parameters={
|
||||
"X-Auth-Token": {
|
||||
"in": "header",
|
||||
"name": "X-Auth-Token",
|
||||
"description": "A valid authentication token",
|
||||
"schema": {"type": "string", "format": "secret"},
|
||||
},
|
||||
"X-Subject-Token": {
|
||||
"in": "header",
|
||||
"name": "X-Subject-Token",
|
||||
"description": "The authentication token. An authentication response returns the token ID in this header rather than in the response body.",
|
||||
"schema": {"type": "string", "format": "secret"},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
for route in self.router.iter_rules():
|
||||
if route.rule.startswith("/static"):
|
||||
continue
|
||||
# if not route.rule.startswith("/v3/domains"):
|
||||
# continue
|
||||
if "/credentials/OS-EC2" in route.rule:
|
||||
continue
|
||||
|
||||
self._process_route(route, openapi_spec)
|
||||
|
||||
self._sanitize_param_ver_info(openapi_spec, self.min_api_version)
|
||||
|
||||
if args.api_ref_src:
|
||||
merge_api_ref_doc(
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
|
||||
return impl_path
|
||||
|
||||
def _process_route(self, route, openapi_spec):
|
||||
args = route.arguments
|
||||
# ep = route.endpoint
|
||||
view = self.app.view_functions[route.endpoint]
|
||||
controller = None
|
||||
if hasattr(view, "view_class"):
|
||||
controller = view.view_class
|
||||
|
||||
path = ""
|
||||
path_elements = []
|
||||
operation_spec = None
|
||||
tag_name = None
|
||||
|
||||
for part in route.rule.split("/"):
|
||||
if not part:
|
||||
continue
|
||||
if part.startswith("<"):
|
||||
param = part.strip("<>").split(":")
|
||||
path_elements.append("{" + param[-1] + "}")
|
||||
else:
|
||||
if not tag_name and part != "" and part != "v3":
|
||||
tag_name = part
|
||||
path_elements.append(part)
|
||||
|
||||
if not tag_name:
|
||||
tag_name = "versions"
|
||||
|
||||
path = "/" + "/".join(path_elements)
|
||||
if tag_name not in [x["name"] for x in openapi_spec.tags]:
|
||||
openapi_spec.tags.append(
|
||||
{"name": tag_name, "description": LiteralScalarString("")}
|
||||
)
|
||||
# Get rid of /v3 for further processing
|
||||
path_elements = path_elements[1:]
|
||||
|
||||
# Build path parameters (/foo/{foo_id}/bar/{id} => $foo_id, $foo_bar_id)
|
||||
# Since for same path we are here multiple times check presence of
|
||||
# parameter before adding new params
|
||||
path_params: list[ParameterSchema] = []
|
||||
path_resource_names: list[str] = []
|
||||
for path_element in path_elements:
|
||||
if "{" in path_element:
|
||||
param_name = path_element.strip("{}")
|
||||
global_param_name = (
|
||||
"_".join(path_resource_names) + f"_{param_name}"
|
||||
)
|
||||
# if global_param_name == "_project_id":
|
||||
# global_param_name = "project_id"
|
||||
param_ref_name = f"#/components/parameters/{global_param_name}"
|
||||
# Ensure reference to the param is in the path_params
|
||||
if param_ref_name not in [
|
||||
k.ref for k in [p for p in path_params]
|
||||
]:
|
||||
path_params.append(ParameterSchema(ref=param_ref_name))
|
||||
# Ensure global parameter is present
|
||||
path_param = ParameterSchema(
|
||||
location="path", name=param_name, required=True
|
||||
)
|
||||
# openapi_spec.components.parameters.setdefault(global_param_name, dict())
|
||||
if not path_param.description:
|
||||
path_param.description = LiteralScalarString(
|
||||
f"{param_name} parameter for {path} API"
|
||||
)
|
||||
# We can only assume the param type. For path it is logically a string only
|
||||
path_param.type_schema = TypeSchema(type="string")
|
||||
openapi_spec.components.parameters[global_param_name] = (
|
||||
path_param
|
||||
)
|
||||
else:
|
||||
path_resource_names.append(path_element.replace("-", "_"))
|
||||
if len(path_elements) == 0:
|
||||
path_resource_names.append("root")
|
||||
elif path_elements[-1].startswith("{"):
|
||||
rn = path_resource_names[-1]
|
||||
if rn.endswith("ies"):
|
||||
rn = rn.replace("ies", "y")
|
||||
else:
|
||||
rn = rn.rstrip("s")
|
||||
path_resource_names[-1] = rn
|
||||
if path == "/v3/domains/{domain_id}/config/{group}":
|
||||
path_resource_names.append("group")
|
||||
elif path == "/v3/domains/config/{group}/{option}/default":
|
||||
path_resource_names.append("group")
|
||||
elif path == "/v3/domains/{domain_id}/config/{group}/{option}":
|
||||
path_resource_names.extend(["group", "option"])
|
||||
|
||||
path_spec = openapi_spec.paths.setdefault(
|
||||
path, PathSchema(parameters=path_params)
|
||||
)
|
||||
# Set operationId
|
||||
if path == "/":
|
||||
operation_id_prefix = "versions"
|
||||
elif path == "/v3":
|
||||
operation_id_prefix = "version"
|
||||
else:
|
||||
operation_id_prefix = "/".join(
|
||||
[x.strip("{}") for x in path_elements]
|
||||
)
|
||||
for method in route.methods:
|
||||
if method == "OPTIONS":
|
||||
# Not sure what should be done with it
|
||||
continue
|
||||
if controller:
|
||||
func = getattr(
|
||||
controller, method.replace("HEAD", "GET").lower(), None
|
||||
)
|
||||
else:
|
||||
func = view
|
||||
# Set operationId
|
||||
operation_id = operation_id_prefix + f":{method.lower()}" # noqa
|
||||
# There is a variety of operations that make absolutely no sense and
|
||||
# are just not filtered by Keystone itself
|
||||
if path == "/v3/users/{user_id}/password" and method in [
|
||||
"GET",
|
||||
"HEAD",
|
||||
]:
|
||||
continue
|
||||
|
||||
# Current Keystone code is having a bug of exposing same controller
|
||||
# API for both /RESOURCE and /RESOURCE/{ID}. Routing is then
|
||||
# failing to invoke the method because of missing parameter, so
|
||||
# analyse and skip those now.
|
||||
if not func:
|
||||
continue
|
||||
sig = inspect.signature(func)
|
||||
for param in args:
|
||||
if param not in sig.parameters:
|
||||
logging.warn(
|
||||
"Skipping %s:%s because controller does not support parameter %s",
|
||||
path,
|
||||
method,
|
||||
param,
|
||||
)
|
||||
func = None
|
||||
break
|
||||
for param in sig.parameters.values():
|
||||
if (
|
||||
param.name not in ["self"]
|
||||
and param.default == param.empty
|
||||
and param.name not in args
|
||||
):
|
||||
# Param with no default is not a path argument
|
||||
logging.warn(
|
||||
"Skipping %s:%s because controller requires parameter %s not present in path",
|
||||
path,
|
||||
method,
|
||||
param,
|
||||
)
|
||||
func = None
|
||||
break
|
||||
|
||||
if not func:
|
||||
continue
|
||||
|
||||
operation_spec = getattr(path_spec, method.lower())
|
||||
if not operation_spec.operationId:
|
||||
operation_spec.operationId = operation_id
|
||||
doc = inspect.getdoc(func)
|
||||
if not operation_spec.description:
|
||||
operation_spec.description = LiteralScalarString(
|
||||
doc or f"{method} operation on {path}"
|
||||
)
|
||||
if tag_name and tag_name not in operation_spec.tags:
|
||||
operation_spec.tags.append(tag_name)
|
||||
|
||||
self.process_operation(
|
||||
func,
|
||||
path,
|
||||
openapi_spec,
|
||||
operation_spec,
|
||||
path_resource_names,
|
||||
method=method,
|
||||
)
|
||||
|
||||
return operation_spec
|
||||
|
||||
def process_operation(
|
||||
self,
|
||||
func,
|
||||
path,
|
||||
openapi_spec,
|
||||
operation_spec,
|
||||
path_resource_names,
|
||||
*,
|
||||
method=None,
|
||||
):
|
||||
logging.info(
|
||||
"Operation: %s [%s]",
|
||||
path,
|
||||
method,
|
||||
)
|
||||
if method in ["PUT", "POST", "PATCH"]:
|
||||
# This is clearly a modification operation but we know nothing about request
|
||||
schema_name = (
|
||||
"".join([x.title() for x in path_resource_names])
|
||||
+ method.title()
|
||||
+ "Request"
|
||||
)
|
||||
|
||||
(schema_ref, mime_type) = self._get_schema_ref(
|
||||
openapi_spec,
|
||||
schema_name,
|
||||
description=f"Request of the {operation_spec.operationId} operation",
|
||||
)
|
||||
|
||||
if schema_ref:
|
||||
content = operation_spec.requestBody = {"content": {}}
|
||||
content["content"][mime_type] = {
|
||||
"schema": {"$ref": schema_ref}
|
||||
}
|
||||
|
||||
responses_spec = operation_spec.responses
|
||||
# Errors
|
||||
for error in ["403", "404"]:
|
||||
responses_spec.setdefault(str(error), dict(description="Error"))
|
||||
# Response data
|
||||
if method == "POST":
|
||||
response_code = "201"
|
||||
if method == "PUT":
|
||||
response_code = "201"
|
||||
elif method == "DELETE":
|
||||
response_code = "204"
|
||||
else:
|
||||
response_code = "200"
|
||||
if path == "/v3/projects/{project_id}/tags/{value}" and method in [
|
||||
"GET",
|
||||
"HEAD",
|
||||
]:
|
||||
response_code = "204"
|
||||
elif path in [
|
||||
"/v3/projects/{project_id}/users/{user_id}/roles/{role_id}",
|
||||
"/v3/domains/{project_id}/users/{user_id}/roles/{role_id}",
|
||||
] and method in ["GET", "HEAD", "PUT"]:
|
||||
response_code = "204"
|
||||
elif path in [
|
||||
"/v3/projects/{project_id}/groups/{user_id}/roles/{role_id}",
|
||||
"/v3/domains/{project_id}/groups/{user_id}/roles/{role_id}",
|
||||
] and method in ["GET", "HEAD", "PUT"]:
|
||||
response_code = "204"
|
||||
elif path == "/v3/users/{user_id}/password" and method == "POST":
|
||||
response_code = "204"
|
||||
rsp = responses_spec.setdefault(response_code, dict(description="Ok"))
|
||||
if response_code != "204" and method not in ["DELETE", "HEAD"]:
|
||||
# Arrange response placeholder
|
||||
schema_name = (
|
||||
"".join([x.title() for x in path_resource_names])
|
||||
+ method.title()
|
||||
+ "Response"
|
||||
)
|
||||
(schema_ref, mime_type) = self._get_schema_ref(
|
||||
openapi_spec,
|
||||
schema_name,
|
||||
description=f"Response of the {operation_spec.operationId} operation",
|
||||
)
|
||||
|
||||
if schema_ref:
|
||||
rsp["content"] = {mime_type: {"schema": {"$ref": schema_ref}}}
|
||||
|
||||
if path == "/v3/auth/tokens":
|
||||
rsp_headers = rsp.setdefault("headers", {})
|
||||
if method == "POST":
|
||||
openapi_spec.components.headers["X-Subject-Token"] = {
|
||||
"description": "API Authorization token",
|
||||
"schema": {"type": "string"},
|
||||
}
|
||||
rsp_headers.setdefault(
|
||||
"X-Subject-Token",
|
||||
{"$ref": "#/components/headers/X-Subject-Token"},
|
||||
)
|
||||
operation_spec.security = []
|
||||
elif method == "GET":
|
||||
operation_spec.parameters.append(
|
||||
ParameterSchema(
|
||||
ref="#/components/parameters/X-Subject-Token"
|
||||
)
|
||||
)
|
||||
rsp_headers.setdefault(
|
||||
"X-Subject-Token",
|
||||
{"$ref": "#/components/headers/X-Subject-Token"},
|
||||
)
|
||||
|
||||
self._post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path=path
|
||||
)
|
||||
|
||||
def _post_process_operation_hook(
|
||||
self, openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
for resource_mod in self.RESOURCE_MODULES:
|
||||
hook = getattr(resource_mod, "_post_process_operation_hook", None)
|
||||
if hook:
|
||||
hook(openapi_spec, operation_spec, path=path)
|
||||
|
||||
def _get_schema_ref(
|
||||
self,
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
):
|
||||
# Invoke modularized schema _get_schema_ref
|
||||
for resource_mod in self.RESOURCE_MODULES:
|
||||
hook = getattr(resource_mod, "_get_schema_ref", None)
|
||||
if hook:
|
||||
(ref, mime_type, matched) = hook(
|
||||
openapi_spec, name, description, schema_def, action_name
|
||||
)
|
||||
if matched:
|
||||
return (ref, mime_type)
|
||||
|
||||
# Default
|
||||
(ref, mime_type) = super()._get_schema_ref(
|
||||
openapi_spec, name, description, action_name=action_name
|
||||
)
|
||||
|
||||
return (ref, mime_type)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
|
||||
from typing import Any
|
||||
|
||||
from keystone.application_credential import (
|
||||
schema as application_credential_schema,
|
||||
)
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.openapi.keystone_schemas import common
|
||||
|
||||
# Application Credentials
|
||||
APPLICATION_CREDENTIAL_ACCESS_RULES_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_rules": copy.deepcopy(
|
||||
application_credential_schema._access_rules_properties
|
||||
),
|
||||
"links": copy.deepcopy(common.LINKS_SCHEMA),
|
||||
},
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIAL_ACCESS_RULE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_rule": copy.deepcopy(
|
||||
application_credential_schema._access_rules_properties["items"]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIAL_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the application credential.",
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the project the application credential was created for and that authentication requests using this application credential will be scoped to.",
|
||||
},
|
||||
**application_credential_schema._application_credential_properties,
|
||||
},
|
||||
}
|
||||
APPLICATION_CREDENTIAL_SCHEMA["properties"].pop("secret", None)
|
||||
|
||||
APPLICATION_CREDENTIAL_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"application_credential": copy.deepcopy(APPLICATION_CREDENTIAL_SCHEMA)
|
||||
},
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIAL_CREATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"application_credential": copy.deepcopy(
|
||||
application_credential_schema.application_credential_create
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIAL_CREATE_RESPONSE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"application_credential": copy.deepcopy(APPLICATION_CREDENTIAL_SCHEMA)
|
||||
},
|
||||
}
|
||||
# Update `secret` field
|
||||
APPLICATION_CREDENTIAL_CREATE_RESPONSE_SCHEMA["properties"][
|
||||
"application_credential"
|
||||
]["properties"]["secret"] = {
|
||||
"type": "string",
|
||||
"description": "The secret for the application credential, either generated by the server or provided by the user. This is only ever shown once in the response to a create request. It is not stored nor ever shown again. If the secret is lost, a new application credential must be created.",
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIALS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"application_credentials": {
|
||||
"type": "array",
|
||||
"items": copy.deepcopy(APPLICATION_CREDENTIAL_SCHEMA),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
APPLICATION_CREDENTIALS_LIST_PARAMETERS = {
|
||||
"application_credentials_name": {
|
||||
"in": "query",
|
||||
"name": "name",
|
||||
"description": "The name of the application credential. Must be unique to a user.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
if operationId == "users/user_id/application_credentials:get":
|
||||
for (
|
||||
key,
|
||||
val,
|
||||
) in APPLICATION_CREDENTIALS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# ### Application Credentials
|
||||
if name == "UsersAccess_RuleGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIAL_ACCESS_RULE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "UsersAccess_RulesGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIAL_ACCESS_RULES_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "UsersApplication_CredentialsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIALS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"UsersApplication_CredentialGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIAL_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "UsersApplication_CredentialsPostRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIAL_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in "UsersApplication_CredentialsPostResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**APPLICATION_CREDENTIAL_CREATE_RESPONSE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,634 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
|
||||
from typing import Any
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.keystone_schemas import common
|
||||
|
||||
|
||||
SCOPE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "The authorization scope, including the system (Since v3.10), a project, or a domain (Since v3.4). If multiple scopes are specified in the same request (e.g. project and domain or domain and system) an HTTP 400 Bad Request will be returned, as a token cannot be simultaneously scoped to multiple authorization targets. An ID is sufficient to uniquely identify a project but if a project is specified by name, then the domain of the project must also be specified in order to uniquely identify the project by name. A domain scope may be specified by either the domain’s ID or name with equivalent results.",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Project Name",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Project Id",
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Project domain Id",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Project domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Domain id",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
"OS-TRUST:trust": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"system": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all": {"type": "boolean"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
AUTH_TOKEN_ISSUE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"description": "An auth object.",
|
||||
"properties": {
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"description": "An identity object.",
|
||||
"properties": {
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"description": "The authentication method.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"password",
|
||||
"token",
|
||||
"totp",
|
||||
"application_credential",
|
||||
],
|
||||
},
|
||||
},
|
||||
"password": {
|
||||
"type": "object",
|
||||
"description": "The password object, contains the authentication information.",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "A `user` object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User Name",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"description": "User Password",
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"description": "User Domain object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User Domain ID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User Domain Name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"token": {
|
||||
"type": "object",
|
||||
"description": "A `token` object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"description": "Authorization Token value",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
],
|
||||
},
|
||||
"totp": {
|
||||
"type": "object",
|
||||
"description": "Multi Factor Authentication information",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The user ID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The user name",
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"passcode": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"description": "MFA passcode",
|
||||
},
|
||||
},
|
||||
"required": ["passcode"],
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"user",
|
||||
],
|
||||
},
|
||||
"application_credential": {
|
||||
"type": "object",
|
||||
"description": "An application credential object.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"descripion": "The ID of the application credential used for authentication. If not provided, the application credential must be identified by its name and its owning user.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"descripion": "The name of the application credential used for authentication. If provided, must be accompanied by a user object.",
|
||||
},
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"description": "The secret for authenticating the application credential.",
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "A user object, required if an application credential is identified by name and not ID.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The user ID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The user name",
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["secret"],
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"methods",
|
||||
],
|
||||
},
|
||||
"scope": SCOPE_SCHEMA,
|
||||
},
|
||||
"required": [
|
||||
"identity",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_PROJECTS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the domain for the project.",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the project.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the project",
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "If set to true, project is enabled. If set to false, project is disabled.",
|
||||
},
|
||||
"links": copy.deepcopy(common.LINKS_SCHEMA),
|
||||
},
|
||||
},
|
||||
},
|
||||
"links": copy.deepcopy(common.LINKS_SCHEMA),
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_DOMAINS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of the domain.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the domain",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The description of the domain.",
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "If set to true, domain is enabled. If set to false, domain is disabled.",
|
||||
},
|
||||
"links": copy.deepcopy(common.LINKS_SCHEMA),
|
||||
},
|
||||
},
|
||||
},
|
||||
"links": copy.deepcopy(common.LINKS_SCHEMA),
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_SYSTEMS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"system": {
|
||||
"type": "array",
|
||||
"description": "A list of systems to access based on role assignments.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_CATALOG_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"catalog": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The endpoint UUID",
|
||||
},
|
||||
"interface": {
|
||||
"type": "string",
|
||||
"enum": ["public", "internal", "admin"],
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "Region name of the endpoint",
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "The endpoint url",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the service to which the endpoint belongs.",
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The service type, which describes the API implemented by the service",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The service name.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_USER_INFO_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A user object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user UUID",
|
||||
},
|
||||
"name": {"type": "string", "description": "A user name"},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user domain UUID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A user domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
"password_expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "DateTime of the user password expiration",
|
||||
},
|
||||
"OS-FEDERATION": {"type": "object"},
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_TOKEN_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audit_ids": {
|
||||
"type": "array",
|
||||
"description": "A list of one or two audit IDs. An audit ID is a unique, randomly generated, URL-safe string that you can use to track a token. The first audit ID is the current audit ID for the token. The second audit ID is present for only re-scoped tokens and is the audit ID from the token before it was re-scoped. A re- scoped token is one that was exchanged for another token of the same or different scope. You can use these audit IDs to track the use of a token or chain of tokens across multiple requests and endpoints without exposing the token ID to non-privileged users.",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"catalog": {
|
||||
"description": "A catalog object.",
|
||||
**AUTH_CATALOG_SCHEMA["properties"]["catalog"],
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the token expires.",
|
||||
},
|
||||
"issues_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the token was issued.",
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"description": "The authentication methods, which are commonly password, token, or other methods. Indicates the accumulated set of authentication methods that were used to obtain the token. For example, if the token was obtained by password authentication, it contains password. Later, if the token is exchanged by using the token authentication method one or more times, the subsequently created tokens contain both password and token in their methods attribute. Unlike multi-factor authentication, the methods attribute merely indicates the methods that were used to authenticate the user in exchange for a token. The client is responsible for determining the total number of authentication factors.",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"user": copy.deepcopy(AUTH_USER_INFO_SCHEMA),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_SCOPED_TOKEN_SCHEMA: dict[str, Any] = copy.deepcopy(AUTH_TOKEN_SCHEMA)
|
||||
AUTH_SCOPED_TOKEN_SCHEMA["properties"]["token"]["properties"].update(
|
||||
**{
|
||||
"is_domain": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"description": "A domain object including the id and name representing the domain the token is scoped to. This is only included in tokens that are scoped to a domain.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A domain UUID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
"project": {
|
||||
"type": "object",
|
||||
"description": "A project object including the id, name and domain object representing the project the token is scoped to. This is only included in tokens that are scoped to a project.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user domain UUID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A user domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"description": "A list of role objects",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A role UUID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A role name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"system": {
|
||||
"type": "object",
|
||||
"description": 'A system object containing information about which parts of the system the token is scoped to. If the token is scoped to the entire deployment system, the system object will consist of {"all": true}. This is only included in tokens that are scoped to the system.',
|
||||
"additionalProperties": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
AUTH_RECEIPT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"receipt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the token expires.",
|
||||
},
|
||||
"issues_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the token was issued.",
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"description": "The authentication methods, which are commonly password, token, or other methods. Indicates the accumulated set of authentication methods that were used to obtain the token. For example, if the token was obtained by password authentication, it contains password. Later, if the token is exchanged by using the token authentication method one or more times, the subsequently created tokens contain both password and token in their methods attribute. Unlike multi-factor authentication, the methods attribute merely indicates the methods that were used to authenticate the user in exchange for a token. The client is responsible for determining the total number of authentication factors.",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"user": copy.deepcopy(AUTH_USER_INFO_SCHEMA),
|
||||
},
|
||||
},
|
||||
"required_auth_methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "A list of authentication rules that may be used with the auth receipt to complete the authentication process.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
|
||||
if operationId == "auth/tokens:post":
|
||||
(receipt_schema_ref, receipt_mime_type, matched) = _get_schema_ref(
|
||||
openapi_spec, "AuthReceiptSchema"
|
||||
)
|
||||
operation_spec.responses["401"] = {
|
||||
"description": "Unauthorized",
|
||||
"headers": {
|
||||
"Openstack-Auth-Receipt": {
|
||||
"$ref": "#/components/headers/Openstack-Auth-Receipt"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
receipt_mime_type: {"schema": {"$ref": receipt_schema_ref}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
|
||||
# Auth
|
||||
if name == "AuthTokensPostRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**AUTH_TOKEN_ISSUE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in ["AuthTokensGetResponse", "AuthTokensPostResponse"]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**AUTH_SCOPED_TOKEN_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "AuthReceiptSchema":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**AUTH_RECEIPT_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthProjectsGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**AUTH_PROJECTS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthDomainsGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**AUTH_DOMAINS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "AuthSystemGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**AUTH_SYSTEMS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "AuthCatalogGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**AUTH_CATALOG_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
|
||||
from typing import Any
|
||||
|
||||
from keystone.resource import schema as ks_schema
|
||||
|
||||
|
||||
LINK_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "Links to the resources in question. See [API Guide / Links and References](https://docs.openstack.org/api-guide/compute/links_and_references.html) for more info.",
|
||||
"properties": {
|
||||
"href": {"type": "string", "format": "uri"},
|
||||
"rel": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
LINKS_SCHEMA: dict[str, Any] = {
|
||||
"type": "array",
|
||||
"description": "Links to the resources in question. See [API Guide / Links and References](https://docs.openstack.org/api-guide/compute/links_and_references.html) for more info.",
|
||||
"items": copy.deepcopy(LINK_SCHEMA),
|
||||
}
|
||||
|
||||
|
||||
TAG_SCHEMA: dict[str, Any] = copy.deepcopy(
|
||||
ks_schema._project_tag_name_properties
|
||||
)
|
||||
|
||||
TAGS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"tags": ks_schema._project_tags_list_properties},
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from typing import Any
|
||||
|
||||
from keystone.resource import schema as ks_schema
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
|
||||
|
||||
DOMAIN_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "format": "uuid", "readOnly": True},
|
||||
**ks_schema._domain_properties,
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
DOMAINS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"domains": {"type": "array", "items": DOMAIN_SCHEMA}},
|
||||
}
|
||||
|
||||
|
||||
DOMAIN_CONFIG_GROUP_LDAP = {
|
||||
"type": "object",
|
||||
"description": "An ldap object. Required to set the LDAP group configuration options.",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "The LDAP URL.",
|
||||
},
|
||||
"user_tree_dn": {
|
||||
"type": "string",
|
||||
"description": "The base distinguished name (DN) of LDAP, from where all users can be reached. For example, ou=Users,dc=root,dc=org.",
|
||||
},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
DOMAIN_CONFIG_GROUP_IDENTITY = {
|
||||
"type": "object",
|
||||
"description": "An identity object.",
|
||||
"properties": {
|
||||
"driver": {
|
||||
"type": "string",
|
||||
"description": "The Identity backend driver.",
|
||||
},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
DOMAIN_CONFIGS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"description": "A config object.",
|
||||
"properties": {
|
||||
"identity": DOMAIN_CONFIG_GROUP_IDENTITY,
|
||||
"ldap": DOMAIN_CONFIG_GROUP_LDAP,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
DOMAIN_CONFIG_SCHEMA: dict[str, Any] = {
|
||||
"oneOf": [
|
||||
DOMAIN_CONFIG_GROUP_IDENTITY,
|
||||
DOMAIN_CONFIG_GROUP_LDAP,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
pass
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# Domains
|
||||
if name in [
|
||||
"DomainsPostResponse",
|
||||
"DomainGetResponse",
|
||||
"DomainPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"Domain", TypeSchema(**DOMAIN_SCHEMA)
|
||||
)
|
||||
ref = "#/components/schemas/Domain"
|
||||
elif name == "DomainsPostRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**ks_schema.domain_create)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "DomainPatchRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**ks_schema.domain_update)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "DomainsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**DOMAINS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
# Domain Config
|
||||
elif name in [
|
||||
"DomainsConfigDefaultGetResponse",
|
||||
"DomainsConfigGetResponse",
|
||||
"DomainsConfigPutRequest",
|
||||
"DomainsConfigPutResponse",
|
||||
"DomainsConfigPatchResponse",
|
||||
"DomainsConfigPatchRequest",
|
||||
"DomainsConfigPatchResponse",
|
||||
"DomainsConfigDefaultGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"DomainConfig",
|
||||
TypeSchema(**DOMAIN_CONFIGS_SCHEMA),
|
||||
)
|
||||
ref = "#/components/schemas/DomainConfig"
|
||||
elif name in [
|
||||
"DomainsConfigGroupGetResponse",
|
||||
"DomainsConfigGroupPatchRequest",
|
||||
"DomainsConfigGroupPatchResponse",
|
||||
"DomainsConfigGroupPatchResponse",
|
||||
"DomainsConfigGroupPatchResponse",
|
||||
"DomainsConfigDefaultGroupGetResponse",
|
||||
"DomainsConfigGroupOptionPatchResponse",
|
||||
"DomainsConfigGroupOptionGetResponse",
|
||||
"DomainsConfigGroupOptionPatchRequest",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"DomainConfigGroup",
|
||||
TypeSchema(**DOMAIN_CONFIG_SCHEMA),
|
||||
)
|
||||
ref = "#/components/schemas/DomainConfigGroup"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,159 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
|
||||
from typing import Any
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
|
||||
ENDPOINT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Defines whether the service and its endpoints appear in the service catalog.",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the service to which the endpoint belongs.",
|
||||
"readOnly": True,
|
||||
},
|
||||
"interface": {
|
||||
"type": "string",
|
||||
"enum": ["internal", "admin", "public"],
|
||||
"description": "The interface type, which describes the visibility of the Value is: - public. Visible by end users on a publicly available network interface. - internal. Visible by end users on an unmetered internal network interface. - admin. Visible by administrative users on a secure network interface.",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The geographic location of the service endpoint.",
|
||||
"x-openstack": {"max-ver": "3.2"},
|
||||
},
|
||||
"region_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The geographic location of the service ",
|
||||
"x-openstack": {"min-ver": "3.2"},
|
||||
},
|
||||
"service_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The UUID of the service to which the endpoint belongs.",
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "The endpoint URL.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ENDPOINT_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"endpoint": ENDPOINT_SCHEMA},
|
||||
}
|
||||
|
||||
ENDPOINTS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"endpoints": {"type": "array", "items": ENDPOINT_SCHEMA}},
|
||||
}
|
||||
|
||||
ENDPOINTS_LIST_PARAMETERS = {
|
||||
"endpoint_service_id": {
|
||||
"in": "query",
|
||||
"name": "service_id",
|
||||
"description": "Filters the response by a service ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
"endpoint_region_id": {
|
||||
"in": "query",
|
||||
"name": "region",
|
||||
"description": "Filters the response by a region ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
"endpoint_interface": {
|
||||
"in": "query",
|
||||
"name": "interface",
|
||||
"description": "Filters the response by an interface.",
|
||||
"schema": {"type": "string", "enum": ["public", "internal", "admin"]},
|
||||
},
|
||||
}
|
||||
|
||||
ENDPOINT_CREATE_SCHEMA: dict[str, Any] = copy.deepcopy(
|
||||
ENDPOINT_CONTAINER_SCHEMA
|
||||
)
|
||||
ENDPOINT_CREATE_SCHEMA["properties"]["endpoint"]["properties"].pop("id")
|
||||
ENDPOINT_CREATE_SCHEMA["properties"]["endpoint"]["required"] = [
|
||||
"interface",
|
||||
"service_id",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
if operationId == "endpoints:get":
|
||||
for (
|
||||
key,
|
||||
val,
|
||||
) in ENDPOINTS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# ### Endpoints
|
||||
if name == "EndpointsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**ENDPOINTS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"EndpointGetResponse",
|
||||
"EndpointsPostRequest",
|
||||
"EndpointsPostResponse",
|
||||
"EndpointPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"Endpoint",
|
||||
TypeSchema(**ENDPOINT_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = "#/components/schemas/Endpoint"
|
||||
elif name == "EndpointsPostRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**ENDPOINT_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,447 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any
|
||||
|
||||
from jsonref import replace_refs
|
||||
|
||||
from keystone.federation import schema as federation_schema
|
||||
from keystone.federation import utils as federation_mapping_schema
|
||||
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.keystone_schemas import auth
|
||||
|
||||
|
||||
IDENTITY_PROVIDER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Identity Provider unique ID",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The Identity Provider description",
|
||||
},
|
||||
"domain_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID of a domain that is associated with the Identity Provider.",
|
||||
},
|
||||
"authorization_ttl": {
|
||||
"type": "integer",
|
||||
"description": "The length of validity in minutes for group memberships carried over through mapping and persisted in the database.",
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the Identity Provider is enabled or not",
|
||||
},
|
||||
"remote_ids": {
|
||||
"type": "array",
|
||||
"description": "List of the unique Identity Provider’s remote IDs",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"identity_provider": IDENTITY_PROVIDER_SCHEMA},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_CREATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity_provider": federation_schema.identity_provider_create
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_UPDATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity_provider": federation_schema.identity_provider_update
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDERS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity_providers": {
|
||||
"type": "array",
|
||||
"items": IDENTITY_PROVIDER_SCHEMA,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDERS_LIST_PARAMETERS: dict[str, Any] = {
|
||||
"idp_id": {
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "Filter for Identity Providers’ ID attribute",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"idp_enabled": {
|
||||
"in": "query",
|
||||
"name": "enabled",
|
||||
"description": "Filter for Identity Providers’ enabled attribute",
|
||||
"schema": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_PROTOCOL_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The federation protocol ID",
|
||||
},
|
||||
"mapping_id": {"type": "string"},
|
||||
"remote_id_attribute": {"type": "string", "maxLength": 64},
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_PROTOCOL_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"protocol": IDENTITY_PROVIDER_PROTOCOL_SCHEMA},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_PROTOCOLS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocols": {
|
||||
"type": "array",
|
||||
"items": IDENTITY_PROVIDER_PROTOCOL_SCHEMA,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_PROTOCOL_CREATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"protocol": federation_schema.protocol_create},
|
||||
}
|
||||
|
||||
IDENTITY_PROVIDER_PROTOCOL_UPDATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"protocol": federation_schema.protocol_update},
|
||||
}
|
||||
|
||||
MAPPING_PROPERTIES = replace_refs(
|
||||
federation_mapping_schema.MAPPING_SCHEMA, proxies=False
|
||||
)
|
||||
MAPPING_PROPERTIES.pop("definitions", None)
|
||||
MAPPING_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Federation Mapping unique ID",
|
||||
},
|
||||
**MAPPING_PROPERTIES["properties"],
|
||||
},
|
||||
}
|
||||
|
||||
MAPPING_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"mapping": MAPPING_SCHEMA},
|
||||
}
|
||||
|
||||
MAPPINGS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"mappings": {"type": "array", "items": MAPPING_SCHEMA}},
|
||||
}
|
||||
|
||||
MAPPING_CREATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"mapping": MAPPING_PROPERTIES},
|
||||
}
|
||||
|
||||
FEDERATION_SERVICE_PROVIDER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auth_url": {
|
||||
"type": "string",
|
||||
"description": "The URL to authenticate against",
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The description of the Service Provider",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Service Provider unique ID",
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the Service Provider is enabled or not",
|
||||
},
|
||||
"relay_state_prefix": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The prefix of the RelayState SAML attribute",
|
||||
},
|
||||
"sp_url": {
|
||||
"type": "string",
|
||||
"description": "The Service Provider’s URL",
|
||||
},
|
||||
},
|
||||
"required": ["auth_url", "sp_url"],
|
||||
}
|
||||
|
||||
FEDERATION_SERVICE_PROVIDER_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"service_provider": FEDERATION_SERVICE_PROVIDER_SCHEMA},
|
||||
}
|
||||
|
||||
FEDERATION_SERVICE_PROVIDERS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_providers": {
|
||||
"type": "array",
|
||||
"items": FEDERATION_SERVICE_PROVIDER_SCHEMA,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
FEDERATION_SERVICE_PROVIDER_CREATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_provider": federation_schema.service_provider_create
|
||||
},
|
||||
}
|
||||
|
||||
FEDERATION_SERVICE_PROVIDER_UPDATE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_provider": federation_schema.service_provider_update
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
if operationId == "OS-FEDERATION/identity_providers:get":
|
||||
for (
|
||||
key,
|
||||
val,
|
||||
) in IDENTITY_PROVIDERS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
elif operationId in [
|
||||
"OS-FEDERATION/projects:get",
|
||||
"OS-FEDERATION/projects:head",
|
||||
"OS-FEDERATION/domains:get",
|
||||
"OS-FEDERATION/domains:head",
|
||||
"endpoints/endpoint_id/OS-ENDPOINT-POLICY/policy:get",
|
||||
]:
|
||||
operation_spec.deprecated = True
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str | None
|
||||
if name == "Os_FederationProjectsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**auth.AUTH_PROJECTS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationDomainsGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**auth.AUTH_DOMAINS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthOs_FederationSaml2PostRequest",
|
||||
"AuthOs_FederationSaml2EcpPostRequest",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**auth.AUTH_TOKEN_ISSUE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthOs_FederationSaml2PostResponse",
|
||||
"AuthOs_FederationSaml2EcpPostResponse",
|
||||
]:
|
||||
mime_type = "text/xml"
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
type="string",
|
||||
format="xml",
|
||||
descripion="SAML assertion in XML format",
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthOs_FederationWebssoGetResponse",
|
||||
"AuthOs_FederationWebssoPostResponse",
|
||||
"AuthOs_FederationIdentity_ProvidersProtocolsWebssoGetResponse",
|
||||
"AuthOs_FederationIdentity_ProvidersProtocolsWebssoPostResponse",
|
||||
"Os_FederationIdentity_ProvidersProtocolsAuthGetResponse",
|
||||
"Os_FederationIdentity_ProvidersProtocolsAuthPostResponse",
|
||||
]:
|
||||
# Federation based auth returns unscoped token even it is not
|
||||
# described explicitly in apiref
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**auth.AUTH_TOKEN_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"AuthOs_FederationWebssoPostRequest",
|
||||
"AuthOs_FederationIdentity_ProvidersProtocolsWebssoPostRequest",
|
||||
]:
|
||||
ref = None
|
||||
# ### Identity provider
|
||||
elif name == "Os_FederationIdentity_ProvidersGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDERS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationIdentity_ProviderGetResponse",
|
||||
"Os_FederationIdentity_ProviderPutResponse",
|
||||
"Os_FederationIdentity_ProviderPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationIdentity_ProviderPutRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationIdentity_ProviderPatchRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_UPDATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# ### Identity provider protocols
|
||||
elif name == "Os_FederationIdentity_ProvidersProtocolsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_PROTOCOLS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationIdentity_ProvidersProtocolGetResponse",
|
||||
"Os_FederationIdentity_ProvidersProtocolPutResponse",
|
||||
"Os_FederationIdentity_ProvidersProtocolPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_PROTOCOL_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationIdentity_ProvidersProtocolPutRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_PROTOCOL_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationIdentity_ProvidersProtocolPatchRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**IDENTITY_PROVIDER_PROTOCOL_UPDATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# ### Identity provider mapping
|
||||
elif name == "Os_FederationMappingsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**MAPPINGS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationMappingGetResponse",
|
||||
"Os_FederationMappingPutResponse",
|
||||
"Os_FederationMappingPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**MAPPING_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationMappingPutRequest",
|
||||
"Os_FederationMappingPatchRequest",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**MAPPING_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# ### Identity provider service provider
|
||||
elif name == "Os_FederationService_ProvidersGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**FEDERATION_SERVICE_PROVIDERS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"Os_FederationService_ProviderGetResponse",
|
||||
"Os_FederationService_ProviderPutResponse",
|
||||
"Os_FederationService_ProviderPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**FEDERATION_SERVICE_PROVIDER_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationService_ProviderPutRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**FEDERATION_SERVICE_PROVIDER_CREATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "Os_FederationService_ProviderPatchRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**FEDERATION_SERVICE_PROVIDER_UPDATE_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# SAML2 Metadata
|
||||
elif name == "Os_FederationSaml2MetadataGetResponse":
|
||||
mime_type = "text/xml"
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(
|
||||
type="string",
|
||||
format="xml",
|
||||
descripion="Identity Provider metadata information in XML format",
|
||||
),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,124 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any
|
||||
|
||||
from keystone.identity import schema as identity_schema
|
||||
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.keystone_schemas import user
|
||||
|
||||
|
||||
GROUP_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "format": "uuid", "readOnly": True},
|
||||
**identity_schema._group_properties,
|
||||
},
|
||||
}
|
||||
|
||||
GROUP_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"group": GROUP_SCHEMA},
|
||||
}
|
||||
|
||||
GROUPS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"groups": {"type": "array", "items": GROUP_SCHEMA}},
|
||||
}
|
||||
|
||||
GROUPS_LIST_PARAMETERS: dict[str, Any] = {
|
||||
"group_domain_id": {
|
||||
"in": "query",
|
||||
"name": "domain_id",
|
||||
"description": "Filters the response by a domain ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
GROUP_USERS_LIST_PARAMETERS: dict[str, Any] = {
|
||||
"group_user_password_expires_at": {
|
||||
"in": "query",
|
||||
"name": "password_expires_at",
|
||||
"description": "Filter results based on which user passwords have expired. The query should include an operator and a timestamp with a colon (:) separating the two, for example: `password_expires_at={operator}:{timestamp}`.\nValid operators are: `lt`, `lte`, `gt`, `gte`, `eq`, and `neq`.\nValid timestamps are of the form: YYYY-MM-DDTHH:mm:ssZ.",
|
||||
"schema": {"type": "string", "format": "date-time"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
|
||||
if operationId == "groups:get":
|
||||
for key, val in GROUPS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
elif operationId == "groups/group_id/users:get":
|
||||
for key, val in GROUP_USERS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# Groups
|
||||
if name == "GroupsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**GROUPS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"GroupsPostRequest",
|
||||
"GroupsPostResponse",
|
||||
"GroupGetResponse",
|
||||
"GroupPatchRequest",
|
||||
"GroupPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"Group", TypeSchema(**GROUP_CONTAINER_SCHEMA)
|
||||
)
|
||||
ref = "#/components/schemas/Group"
|
||||
elif name == "GroupsUsersGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**user.USERS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"GroupsUserGetResponse",
|
||||
"GroupsUserPutRequest",
|
||||
"GroupsUserPutResponse",
|
||||
]:
|
||||
return (None, None, True)
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,157 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any
|
||||
|
||||
from keystone.resource import schema as ks_schema
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.openapi.keystone_schemas import common
|
||||
|
||||
|
||||
PROJECT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "format": "uuid", "readOnly": True},
|
||||
**ks_schema._project_properties,
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
PROJECT_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "format": "uuid", "readOnly": True},
|
||||
**ks_schema._project_properties,
|
||||
},
|
||||
"additionalProperties": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
PROJECTS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"projects": {"type": "array", "items": PROJECT_SCHEMA}},
|
||||
}
|
||||
|
||||
PROJECT_LIST_PARAMETERS = {
|
||||
"project_domain_id": {
|
||||
"in": "query",
|
||||
"name": "domain_id",
|
||||
"description": "Filters the response by a domain ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
"project_enabled": {
|
||||
"in": "query",
|
||||
"name": "enabled",
|
||||
"description": "If set to true, then only enabled projects will be returned. Any value other than 0 (including no value) will be interpreted as true.",
|
||||
"schema": {"type": "boolean"},
|
||||
},
|
||||
"project_is_domain": {
|
||||
"in": "query",
|
||||
"name": "is_domain",
|
||||
"description": "If this is specified as true, then only projects acting as a domain are included. Otherwise, only projects that are not acting as a domain are included.",
|
||||
"schema": {"type": "boolean"},
|
||||
"x-openstack": {"min-ver": "3.6"},
|
||||
},
|
||||
"project_name": {
|
||||
"in": "query",
|
||||
"name": "name",
|
||||
"description": "Filters the response by a resource name.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"project_parent_id": {
|
||||
"in": "query",
|
||||
"name": "parent_id",
|
||||
"description": "Filters the response by a parent ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
"x-openstack": {"min-ver": "3.4"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
if operationId == "projects:get":
|
||||
for (
|
||||
key,
|
||||
val,
|
||||
) in PROJECT_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# Projects
|
||||
if name in [
|
||||
"ProjectsPostRequest",
|
||||
"ProjectsPostResponse",
|
||||
"ProjectPatchRequest",
|
||||
"ProjectPatchResponse",
|
||||
"ProjectGetResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"Project",
|
||||
TypeSchema(**PROJECT_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = "#/components/schemas/Project"
|
||||
elif name == "ProjectsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**PROJECTS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
# Project Tags
|
||||
elif name == "ProjectsTagPutRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**ks_schema.project_tag_create)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ProjectsTagsPutRequest":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**ks_schema.project_tags_update)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ProjectsTagsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**common.TAGS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name == "ProjectsTagsPutResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**common.TAGS_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,109 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
|
||||
REGION_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The region description.",
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The ID for the region.",
|
||||
"readOnly": True,
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "To make this region a child of another region, set this parameter to the ID of the parent region.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
REGION_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"region": REGION_SCHEMA},
|
||||
}
|
||||
|
||||
REGIONS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"regions": {"type": "array", "items": REGION_SCHEMA}},
|
||||
}
|
||||
|
||||
REGIONS_LIST_PARAMETERS = {
|
||||
"region_parent_region_id": {
|
||||
"in": "query",
|
||||
"name": "parent_region_id",
|
||||
"description": "Filters the response by a parent region, by ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path: str | None = None
|
||||
):
|
||||
"""Hook to allow service specific generator to modify details"""
|
||||
operationId = operation_spec.operationId
|
||||
if operationId == "regions:get":
|
||||
for (
|
||||
key,
|
||||
val,
|
||||
) in REGIONS_LIST_PARAMETERS.items():
|
||||
openapi_spec.components.parameters.setdefault(
|
||||
key, ParameterSchema(**val)
|
||||
)
|
||||
ref = f"#/components/parameters/{key}"
|
||||
if ref not in [x.ref for x in operation_spec.parameters]:
|
||||
operation_spec.parameters.append(ParameterSchema(ref=ref))
|
||||
|
||||
|
||||
def _get_schema_ref(
|
||||
openapi_spec,
|
||||
name,
|
||||
description=None,
|
||||
schema_def=None,
|
||||
action_name=None,
|
||||
) -> tuple[str | None, str | None, bool]:
|
||||
mime_type: str = "application/json"
|
||||
ref: str
|
||||
# ### Regions
|
||||
if name == "RegionsGetResponse":
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
name,
|
||||
TypeSchema(**REGIONS_SCHEMA),
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
elif name in [
|
||||
"RegionGetResponse",
|
||||
"RegionsPostRequest",
|
||||
"RegionsPostResponse",
|
||||
"RegionPatchRequest",
|
||||
"RegionPatchResponse",
|
||||
]:
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
"Region",
|
||||
TypeSchema(**REGION_CONTAINER_SCHEMA),
|
||||
)
|
||||
ref = "#/components/schemas/Region"
|
||||
|
||||
else:
|
||||
return (None, None, False)
|
||||
|
||||
return (ref, mime_type, True)
|
||||
@@ -0,0 +1,436 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from typing import Any
|
||||
|
||||
from keystone.assignment import schema as assignment_schema
|
||||
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.openapi.keystone_schemas import auth
|
||||
|
||||
ROLE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A role object.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The role ID.",
|
||||
"readOnly": True,
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri",
|
||||
},
|
||||
"readOnly": True,
|
||||
},
|
||||
**assignment_schema._role_properties,
|
||||
},
|
||||
}
|
||||
|
||||
ROLE_INFO_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The role ID.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The role name.",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The role description.",
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "The link to the resource in question.",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ROLE_CONTAINER_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"role": ROLE_SCHEMA},
|
||||
}
|
||||
|
||||
ROLES_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roles": {"type": "array", "items": ROLE_SCHEMA},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# List of role info
|
||||
ROLES_INFO_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "List of roles assigned to the resource",
|
||||
"properties": {
|
||||
"roles": {"type": "array", "items": ROLE_INFO_SCHEMA},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Role list specific query parameters
|
||||
ROLE_LIST_PARAMETERS: dict[str, Any] = {
|
||||
"role_domain_id": {
|
||||
"in": "query",
|
||||
"name": "domain_id",
|
||||
"description": "Filters the response by a domain ID.",
|
||||
"schema": {"type": "string", "format": "uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
ROLE_INFERENCE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role_inference": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prior_role": ROLE_INFO_SCHEMA,
|
||||
"implies": ROLE_INFO_SCHEMA,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ROLES_INFERENCE_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role_inference": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prior_role": ROLE_INFO_SCHEMA,
|
||||
"implies": {
|
||||
"type": "array",
|
||||
"items": ROLE_INFO_SCHEMA,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ROLES_INFERENCES_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role_inferences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prior_role": ROLE_INFO_SCHEMA,
|
||||
"implies": {
|
||||
"type": "array",
|
||||
"items": ROLE_INFO_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
USER_INFO_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "A user object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user UUID",
|
||||
},
|
||||
"name": {"type": "string", "description": "A user name"},
|
||||
"domain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user domain UUID",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A user domain name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
GROUP_INFO_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "format": "uuid", "description": "A user ID"},
|
||||
"name": {"type": "string", "description": "A user name"},
|
||||
},
|
||||
}
|
||||
|
||||
ROLE_ASSIGNMENT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": ROLE_INFO_SCHEMA,
|
||||
"scope": auth.SCOPE_SCHEMA,
|
||||
"user": USER_INFO_SCHEMA,
|
||||
"group": GROUP_INFO_SCHEMA,
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assignment": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "a link to the assignment that gave rise to this entity",
|
||||
},
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ROLE_ASSIGNMENTS_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||