Start building Ironic OpenAPI
Change-Id: If1d9a47a9a87c985988b3378cddf0330d14b9458
This commit is contained in:
parent
ee8925c71c
commit
8f5dc2e994
@ -1309,6 +1309,48 @@ class OpenStackServerSourceBase:
|
||||
getattr(f, "_request_query_schema", {}),
|
||||
)
|
||||
query_params_versions.append((obj, min_ver, max_ver))
|
||||
if "validators" in closure_locals:
|
||||
validators = closure_locals.get("validators")
|
||||
body_schemas = []
|
||||
if isinstance(validators, dict):
|
||||
for k, v in validators.items():
|
||||
sig = inspect.signature(v)
|
||||
vals = sig.parameters.get("validators", None)
|
||||
if vals:
|
||||
print(vals)
|
||||
sig2 = inspect.signature(vals.default[0])
|
||||
schema_param = sig2.parameters.get("schema", None)
|
||||
if schema_param:
|
||||
schema = schema_param.default
|
||||
|
||||
typ_name = (
|
||||
"".join(
|
||||
[
|
||||
x.title()
|
||||
for x in path_resource_names
|
||||
]
|
||||
)
|
||||
+ func.__name__.title()
|
||||
+ (
|
||||
f"_{min_ver.replace('.', '')}"
|
||||
if min_ver
|
||||
else ""
|
||||
)
|
||||
)
|
||||
comp_schema = (
|
||||
openapi_spec.components.schemas.setdefault(
|
||||
typ_name,
|
||||
self._sanitize_schema(
|
||||
copy.deepcopy(schema),
|
||||
start_version=None,
|
||||
end_version=None,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
ref_name = f"#/components/schemas/{typ_name}"
|
||||
if isinstance(body_schemas, list):
|
||||
body_schemas.append(ref_name)
|
||||
|
||||
f = f.__wrapped__
|
||||
|
||||
|
@ -159,7 +159,7 @@ class CinderV3Generator(OpenStackServerSourceBase):
|
||||
if route.routepath.startswith(
|
||||
"/extensions"
|
||||
) or route.routepath.startswith(
|
||||
"/{project_id:[0-9a-f\-]+}/extensions"
|
||||
"/{project_id:[0-9a-f\\-]+}/extensions"
|
||||
):
|
||||
if route.defaults.get("action") != "index":
|
||||
# Extensions controller is broken as one exposing CRUD
|
||||
|
201
codegenerator/openapi/ironic.py
Normal file
201
codegenerator/openapi/ironic.py
Normal file
@ -0,0 +1,201 @@
|
||||
# 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
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import fixtures
|
||||
|
||||
from codegenerator.common.schema import SpecSchema
|
||||
from codegenerator.openapi.base import OpenStackServerSourceBase
|
||||
from codegenerator.openapi.utils import merge_api_ref_doc
|
||||
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
|
||||
class IronicGenerator(OpenStackServerSourceBase):
|
||||
URL_TAG_MAP = {}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
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=""):
|
||||
resource: str | None = None
|
||||
# Construct resource name from the path
|
||||
parent = path.split("/")[-1]
|
||||
if parent == "v1":
|
||||
resource = ""
|
||||
elif parent.endswith("ies"):
|
||||
resource = parent[0 : len(parent) - 3] + "y"
|
||||
elif parent in ["allocation", "history", "vmedia", "chassis", "bios"]:
|
||||
resource = parent
|
||||
else:
|
||||
resource = parent[0:-1]
|
||||
|
||||
for part in [x for x in dir(node) if callable(getattr(node, x))]:
|
||||
# Iterate over functions to find what is exposed on the current
|
||||
# level
|
||||
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]
|
||||
# if path.startswith("/v2/lbaas/quotas"):
|
||||
# # Hack path parameter name for quotas
|
||||
# resource = "project"
|
||||
# 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 == "patch":
|
||||
conditions["method"] = ["PATCH"]
|
||||
action = "update"
|
||||
url += f"/{{{resource}_id}}"
|
||||
elif part == "delete":
|
||||
conditions["method"] = ["DELETE"]
|
||||
action = "delete"
|
||||
url += f"/{{{resource}_id}}"
|
||||
elif part in getattr(node, "_custom_actions", {}):
|
||||
conditions["method"] = getattr(
|
||||
node, "_custom_actions", {}
|
||||
)[part]
|
||||
action = part
|
||||
url += f"/{part}"
|
||||
|
||||
if action:
|
||||
# If we identified method as "interesting" register it into
|
||||
# the routes mapper
|
||||
mapper.connect(
|
||||
None,
|
||||
url,
|
||||
controller=obj,
|
||||
action=action,
|
||||
conditions=conditions,
|
||||
)
|
||||
|
||||
for subcontroller, v in getattr(
|
||||
node, "_subcontroller_map", {}
|
||||
).items():
|
||||
if resource:
|
||||
subpath = f"{path}/{{{resource}_id}}/{subcontroller}"
|
||||
else:
|
||||
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 Octavia OpenAPI schema")
|
||||
|
||||
def _generate(self, target_dir, args):
|
||||
from ironic.api.controllers.v1 import versions
|
||||
from ironic.api.controllers import root as root_controller
|
||||
from ironic.api.controllers import v1
|
||||
|
||||
from pecan import make_app as pecan_make_app
|
||||
from routes import Mapper
|
||||
|
||||
self.api_version = versions.max_version_string()
|
||||
self.min_api_version = versions.min_version_string()
|
||||
|
||||
work_dir = Path(target_dir)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
impl_path = Path(
|
||||
work_dir, "openapi_specs", "baremetal", 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 Baremetal API",
|
||||
"description": LiteralScalarString(
|
||||
"Baremetal API provided by Ironic service"
|
||||
),
|
||||
"version": self.api_version,
|
||||
},
|
||||
openapi="3.1.0",
|
||||
security=[{"ApiKeyAuth": []}],
|
||||
components={
|
||||
"securitySchemes": {
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Auth-Token",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.app = pecan_make_app(root_controller.RootController())
|
||||
self.root = self.app.application.root
|
||||
mapper = Mapper()
|
||||
self._build_routes(mapper, v1.Controller, "/v1")
|
||||
|
||||
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)
|
||||
|
||||
lnk = Path(impl_path.parent, "v1.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
lnk.symlink_to(impl_path.name)
|
||||
|
||||
return impl_path
|
@ -73,6 +73,11 @@ class OpenApiSchemaGenerator(BaseGenerator):
|
||||
|
||||
DesignateGenerator().generate(target_dir, args)
|
||||
|
||||
def generate_ironic(self, target_dir, args):
|
||||
from codegenerator.openapi.ironic import IronicGenerator
|
||||
|
||||
IronicGenerator().generate(target_dir, args)
|
||||
|
||||
def generate(
|
||||
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
||||
):
|
||||
@ -83,6 +88,8 @@ class OpenApiSchemaGenerator(BaseGenerator):
|
||||
# dramatically
|
||||
if args.service_type == "compute":
|
||||
self.generate_nova(target_dir, args)
|
||||
elif args.service_type == "baremetal":
|
||||
self.generate_ironic(target_dir, args)
|
||||
elif args.service_type in ["block-storage", "volume"]:
|
||||
self.generate_cinder(target_dir, args)
|
||||
elif args.service_type == "dns":
|
||||
|
@ -45,6 +45,8 @@ placement =
|
||||
openstack-placement>=10.0
|
||||
shared-file-system =
|
||||
manila>=18.0
|
||||
baremetal =
|
||||
ironic>=26.0
|
||||
|
||||
[mypy]
|
||||
show_column_numbers = true
|
||||
|
@ -38,3 +38,7 @@ fi
|
||||
if [ -z "$1" -o "$1" = "dns" ]; then
|
||||
openstack-codegenerator --work-dir wrk --target openapi-spec --service-type dns --api-ref-src ${API_REF_BUILD_ROOT}/designate/api-ref/build/html/dns-api-v2-index.html --validate
|
||||
fi
|
||||
|
||||
if [ -z "$1" -o "$1" = "baremetal" ]; then
|
||||
openstack-codegenerator --work-dir wrk --target openapi-spec --service-type baremetal --api-ref-src ${API_REF_BUILD_ROOT}/ironic/api-ref/build/html/index.html --validate
|
||||
fi
|
||||
|
@ -33,6 +33,35 @@
|
||||
codegenerator_work_dir: "wrk"
|
||||
install_additional_projects: []
|
||||
|
||||
- job:
|
||||
name: codegenerator-openapi-baremetal-tips
|
||||
parent: codegenerator-openapi-tips-base
|
||||
description: |
|
||||
Generate OpenAPI spec for Ironic
|
||||
required-projects:
|
||||
- name: openstack/ironic
|
||||
|
||||
vars:
|
||||
openapi_service: baremetal
|
||||
install_additional_projects:
|
||||
- project: "opendev.org/openstack/ironic"
|
||||
name: "."
|
||||
|
||||
- job:
|
||||
name: codegenerator-openapi-baremetal-tips-with-api-ref
|
||||
parent: codegenerator-openapi-baremetal-tips
|
||||
description: |
|
||||
Generate OpenAPI spec for Ironic consuming API-REF
|
||||
required-projects:
|
||||
- name: openstack/ironic
|
||||
|
||||
pre-run:
|
||||
- playbooks/openapi/pre-api-ref.yaml
|
||||
vars:
|
||||
codegenerator_api_ref:
|
||||
project: "opendev.org/openstack/ironic"
|
||||
path: "/api-ref/build/html/index.html"
|
||||
|
||||
- job:
|
||||
name: codegenerator-openapi-block-storage-tips
|
||||
parent: codegenerator-openapi-tips-base
|
||||
@ -315,6 +344,8 @@
|
||||
description: |
|
||||
Published OpenAPI specs
|
||||
dependencies:
|
||||
- name: codegenerator-openapi-baremetal-tips-with-api-ref
|
||||
soft: true
|
||||
- name: codegenerator-openapi-block-storage-tips-with-api-ref
|
||||
soft: true
|
||||
- name: codegenerator-openapi-compute-tips-with-api-ref
|
||||
|
@ -6,6 +6,7 @@
|
||||
jobs:
|
||||
- openstack-tox-pep8
|
||||
- openstack-tox-py311
|
||||
- codegenerator-openapi-baremetal-tips-with-api-ref
|
||||
- codegenerator-openapi-block-storage-tips-with-api-ref
|
||||
- codegenerator-openapi-compute-tips-with-api-ref
|
||||
- codegenerator-openapi-dns-tips-with-api-ref
|
||||
@ -22,6 +23,7 @@
|
||||
jobs:
|
||||
- openstack-tox-pep8
|
||||
- openstack-tox-py311
|
||||
- codegenerator-openapi-baremetal-tips-with-api-ref
|
||||
- codegenerator-openapi-block-storage-tips-with-api-ref
|
||||
- codegenerator-openapi-compute-tips-with-api-ref
|
||||
- codegenerator-openapi-dns-tips-with-api-ref
|
||||
|
Loading…
x
Reference in New Issue
Block a user