Merge "Bootstrap magnum OpenAPI build"

This commit is contained in:
Zuul 2025-03-05 12:01:08 +00:00 committed by Gerrit Code Review
commit 02d674d362
7 changed files with 373 additions and 5 deletions

@ -25,6 +25,9 @@ from codegenerator.common.schema import SpecSchema
from codegenerator.metadata.base import MetadataBase
from codegenerator.metadata.baremetal import BaremetalMetadata
from codegenerator.metadata.block_storage import BlockStorageMetadata
from codegenerator.metadata.container_infrastructure_management import (
ContainerInfrastructureManagementMetadata,
)
from codegenerator.metadata.compute import ComputeMetadata
from codegenerator.metadata.dns import DnsMetadata
from codegenerator.metadata.identity import IdentityMetadata
@ -55,6 +58,7 @@ SERVICE_METADATA_MAP: dict[str, ty.Type[MetadataBase]] = {
"block-storage": BlockStorageMetadata,
"volume": BlockStorageMetadata,
"compute": ComputeMetadata,
"container-infrastructure-management": ContainerInfrastructureManagementMetadata,
"dns": DnsMetadata,
"identity": IdentityMetadata,
"image": ImageMetadata,

@ -0,0 +1,33 @@
# 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 typing as ty
from codegenerator.types import OperationModel
from codegenerator.metadata.base import MetadataBase
class ContainerInfrastructureManagementMetadata(MetadataBase):
@staticmethod
def get_operation_key(
operation, path: str, method: str, resource_name: str
) -> ty.Tuple[str | None, bool]:
skip: bool = False
operation_key: str | None = None
return (operation_key, skip)
@staticmethod
def post_process_operation(
resource_name: str, operation_name: str, operation
):
return operation

@ -285,8 +285,8 @@ class OpenStackServerSourceBase:
versioned_methods = {}
controller_actions = {}
framework = None
if hasattr(controller, "controller"):
# framework = None
if hasattr(controller, "controller") and framework != "pecan":
# wsgi
framework = "wsgi"
contr = controller.controller
@ -304,9 +304,13 @@ class OpenStackServerSourceBase:
# Pecan base app
framework = "pecan"
contr = controller
if hasattr(controller, "versioned_methods"):
versioned_methods = contr.versioned_methods
else:
raise RuntimeError(f"Unsupported controller {controller}")
raise RuntimeError(
f"Unsupported controller {controller} {framework}"
)
# logging.debug("Actions: %s, Versioned methods: %s", actions, versioned_methods)
# path_spec = openapi_spec.paths.setdefault(path, PathSchema())
@ -317,7 +321,6 @@ class OpenStackServerSourceBase:
if path_elements and VERSION_RE.match(path_elements[0]):
path_elements.pop(0)
operation_tags = self._get_tags_for_url(path)
print(f"tags={operation_tags} for {path}")
# 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
@ -745,7 +748,14 @@ class OpenStackServerSourceBase:
body_spec = getattr(fdef, "body_type", None)
if body_spec:
body_schema = _convert_wsme_to_jsonschema(body_spec)
schema_name = body_spec.__name__
if hasattr(body_spec, "__name__"):
schema_name = body_spec.__name__
else:
schema_name = (
"".join([x.title() for x in path_resource_names])
+ func.__name__.title()
+ "Request"
)
openapi_spec.components.schemas.setdefault(
schema_name, TypeSchema(**body_schema)
)

@ -0,0 +1,277 @@
# 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
import logging
from multiprocessing import Process
from pathlib import Path
from typing import Any
from unittest import mock
import fixtures
from codegenerator.common.schema import SpecSchema
from codegenerator.common.schema import TypeSchema
from codegenerator.openapi.base import (
OpenStackServerSourceBase,
_convert_wsme_to_jsonschema,
)
from codegenerator.openapi.utils import merge_api_ref_doc
from ruamel.yaml.scalarstring import LiteralScalarString
class MagnumGenerator(OpenStackServerSourceBase):
URL_TAG_MAP = {}
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 _build_routes(self, mapper, node, path=""):
if hasattr(node, "versioned_methods"):
resource = None
parent = path.split("/")[-1]
# Construct resource name from the path
if parent.endswith("ies"):
resource = parent[0 : len(parent) - 3] + "y"
else:
resource = parent[0:-1]
for method, vers in node.versioned_methods.items():
url = path
if method == "post":
conditions = {"method": ["POST"]}
elif method == "patch":
conditions = {"method": ["PATCH"]}
url += f"/{{{resource}_id}}"
elif method == "delete":
conditions = {"method": ["DELETE"]}
elif method == "get":
conditions = {"method": ["GET"]}
else:
conditions = {"method": ["POST"]}
if method in getattr(node, "_custom_actions", []):
url += f"/{method}"
conditions = {
"method": getattr(node, "_custom_actions")[method]
}
mapper.connect(
url,
controller=getattr(node, method),
action=method,
conditions=conditions,
)
for part in dir(node):
if part.startswith("_"):
continue
try:
if callable(getattr(node, part)):
# Iterate over functions to find what is exposed on the current
# level
# if part == "versioned_methods"
obj = getattr(node, part)
_pecan = getattr(obj, "_pecan", None)
exposed = getattr(obj, "exposed", None)
if _pecan and exposed:
# Only whatever is pecan exposed is of interest
conditions = {}
action = None
url = path
resource = None
parent = url.split("/")[-1]
# Construct resource name from the path
if parent.endswith("ies"):
resource = parent[0 : len(parent) - 3] + "y"
else:
resource = parent[0:-1]
# Identify the action from function name
# https://pecan.readthedocs.io/en/latest/rest.html#url-mapping
if part == "get_one":
conditions["method"] = ["GET"]
action = "show"
url += f"/{{{resource}_id}}"
elif part == "get_all":
conditions["method"] = ["GET"]
action = "list"
elif part == "get":
conditions["method"] = ["GET"]
action = "get"
# "Get" is tricky, it can be normal and root, so need to inspect params
sig = inspect.signature(obj)
for pname, pval in sig.parameters.items():
if (
"id" in pname
and pval.default == pval.empty
):
url += f"/{{{resource}_id}}"
elif part == "post":
conditions["method"] = ["POST"]
action = "create"
# url += f"/{{{resource}_id}}"
elif part == "put":
conditions["method"] = ["PUT"]
action = "update"
url += f"/{{{resource}_id}}"
elif part == "delete":
conditions["method"] = ["DELETE"]
action = "delete"
url += f"/{{{resource}_id}}"
if action:
# If we identified method as "interesting" register it into
# the routes mapper
mapper.connect(
None,
url,
controller=obj,
action=action,
conditions=conditions,
)
# yield part
except Exception as ex:
logging.debug(f"method {part} is not callable due to {ex}")
pass
if not hasattr(node, "__dict__"):
return
for subcontroller, v in node.__class__.__dict__.items():
# Iterate over node attributes for subcontrollers
if subcontroller.startswith("_"):
continue
if subcontroller in ["__wrapped__", "__doc__"]:
# Not underested in those
continue
subpath = f"{path}/{subcontroller}"
self._build_routes(mapper, v, subpath)
return
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 Magnum OpenAPI schema")
def _generate(self, target_dir, args):
import pecan.testing
from magnum.api.controllers import versions
from magnum.api.controllers.v1 import Controller
from magnum.api import app
from oslo_config import cfg
self.min_api_version = versions.BASE_VER
self.api_version = versions.CURRENT_MAX_VER
from pecan import make_app as pecan_make_app
from routes import Mapper
work_dir = Path(target_dir)
work_dir.mkdir(parents=True, exist_ok=True)
impl_path = Path(
work_dir,
"openapi_specs",
"container-infrastructure-management",
f"v{self.api_version}.yaml",
)
impl_path.parent.mkdir(parents=True, exist_ok=True)
openapi_spec = self.load_openapi(Path(impl_path))
if not openapi_spec:
openapi_spec = SpecSchema(
info={
"title": "OpenStack Container Managent Infrastructure API",
"description": LiteralScalarString(
"Container Management Infrastructure API provided by Magnum service"
),
"version": self.api_version,
},
openapi="3.1.0",
security=[{"ApiKeyAuth": []}],
components={
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-Auth-Token",
}
}
},
)
with mock.patch("pecan.request") as m:
self.app = app.setup_app()
self.root = self.app.application.app.root
mapper = Mapper()
self._build_routes(mapper, self.root)
for route in mapper.matchlist:
self._process_route(route, openapi_spec, framework="pecan")
if args.api_ref_src:
merge_api_ref_doc(
openapi_spec, args.api_ref_src, allow_strip_version=False
)
self.dump_openapi(
openapi_spec,
Path(impl_path),
args.validate,
"container-infrastructure-management",
)
lnk = Path(impl_path.parent, "v1.yaml")
lnk.unlink(missing_ok=True)
lnk.symlink_to(impl_path.name)
return impl_path
def _get_schema_ref(
self,
openapi_spec,
name,
description=None,
schema_def=None,
action_name=None,
):
schema: None = None
ref: str | None
mime_type: str | None = "application/json"
if name in [
"LbaasLoadbalancersFailoverFailoverRequest",
"OctaviaAmphoraeFailoverFailoverRequest",
]:
schema = openapi_spec.components.schemas.setdefault(
name, TypeSchema(type="null")
)
ref = f"#/components/schemas/{name}"
else:
(ref, mime_type) = super()._get_schema_ref(
openapi_spec,
name,
description,
schema_def=schema_def,
action_name=action_name,
)
return (ref, mime_type)

@ -53,6 +53,11 @@ class OpenApiSchemaGenerator(BaseGenerator):
OctaviaGenerator().generate(target_dir, args)
def generate_magnum(self, target_dir, args):
from codegenerator.openapi.magnum import MagnumGenerator
MagnumGenerator().generate(target_dir, args)
def generate_neutron(self, target_dir, args):
from codegenerator.openapi.neutron import NeutronGenerator
@ -97,6 +102,12 @@ class OpenApiSchemaGenerator(BaseGenerator):
self.generate_ironic(target_dir, args)
elif args.service_type in ["block-storage", "volume"]:
self.generate_cinder(target_dir, args)
elif args.service_type in [
"container-infrastructure-management",
"container-infrastructure",
"container-infra",
]:
self.generate_magnum(target_dir, args)
elif args.service_type == "dns":
self.generate_designate(target_dir, args)
elif args.service_type == "image":

@ -120,6 +120,35 @@
project: "opendev.org/openstack/nova"
path: "/api-ref/build/html/index.html"
- job:
name: codegenerator-openapi-container-infrastructure-management-tips
parent: codegenerator-openapi-tips-base
description: |
Generate OpenAPI spec for Magnum
required-projects:
- name: openstack/magnum
vars:
openapi_service: container-infrastructure-management
install_additional_projects:
- project: "opendev.org/openstack/magnum"
name: "."
- job:
name: codegenerator-openapi-container-infrastructure-management-tips-with-api-ref
parent: codegenerator-openapi-container-infrastructure-management-tips
description: |
Generate OpenAPI spec for Magnum consuming API-REF
required-projects:
- name: openstack/magnum
pre-run:
- playbooks/openapi/pre-api-ref.yaml
vars:
codegenerator_api_ref:
project: "opendev.org/openstack/magnum"
path: "/api-ref/build/html/index.html"
- job:
name: codegenerator-openapi-dns-tips
parent: codegenerator-openapi-tips-base
@ -364,6 +393,8 @@
soft: true
- name: codegenerator-openapi-compute-tips-with-api-ref
soft: true
- name: codegenerator-openapi-container-infrastructure-management-tips-with-api-ref
soft: true
- name: codegenerator-openapi-dns-tips-with-api-ref
soft: true
- name: codegenerator-openapi-identity-tips-with-api-ref

@ -9,6 +9,7 @@
- codegenerator-openapi-baremetal-tips-with-api-ref
- codegenerator-openapi-block-storage-tips-with-api-ref
- codegenerator-openapi-compute-tips-with-api-ref
- codegenerator-openapi-container-infrastructure-management-tips-with-api-ref
- codegenerator-openapi-dns-tips-with-api-ref
- codegenerator-openapi-identity-tips-with-api-ref
- codegenerator-openapi-image-tips-with-api-ref
@ -27,6 +28,7 @@
- codegenerator-openapi-baremetal-tips-with-api-ref
- codegenerator-openapi-block-storage-tips-with-api-ref
- codegenerator-openapi-compute-tips-with-api-ref
- codegenerator-openapi-container-infrastructure-management-tips-with-api-ref
- codegenerator-openapi-dns-tips-with-api-ref
- codegenerator-openapi-identity-tips-with-api-ref
- codegenerator-openapi-image-tips-with-api-ref