Start building Ironic OpenAPI
Change-Id: If1d9a47a9a87c985988b3378cddf0330d14b9458
This commit is contained in:
@@ -1309,6 +1309,48 @@ class OpenStackServerSourceBase:
|
|||||||
getattr(f, "_request_query_schema", {}),
|
getattr(f, "_request_query_schema", {}),
|
||||||
)
|
)
|
||||||
query_params_versions.append((obj, min_ver, max_ver))
|
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__
|
f = f.__wrapped__
|
||||||
|
|
||||||
|
@@ -159,7 +159,7 @@ class CinderV3Generator(OpenStackServerSourceBase):
|
|||||||
if route.routepath.startswith(
|
if route.routepath.startswith(
|
||||||
"/extensions"
|
"/extensions"
|
||||||
) or route.routepath.startswith(
|
) or route.routepath.startswith(
|
||||||
"/{project_id:[0-9a-f\-]+}/extensions"
|
"/{project_id:[0-9a-f\\-]+}/extensions"
|
||||||
):
|
):
|
||||||
if route.defaults.get("action") != "index":
|
if route.defaults.get("action") != "index":
|
||||||
# Extensions controller is broken as one exposing CRUD
|
# 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)
|
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(
|
def generate(
|
||||||
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
|
||||||
):
|
):
|
||||||
@@ -83,6 +88,8 @@ class OpenApiSchemaGenerator(BaseGenerator):
|
|||||||
# dramatically
|
# dramatically
|
||||||
if args.service_type == "compute":
|
if args.service_type == "compute":
|
||||||
self.generate_nova(target_dir, args)
|
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"]:
|
elif args.service_type in ["block-storage", "volume"]:
|
||||||
self.generate_cinder(target_dir, args)
|
self.generate_cinder(target_dir, args)
|
||||||
elif args.service_type == "dns":
|
elif args.service_type == "dns":
|
||||||
|
@@ -45,6 +45,8 @@ placement =
|
|||||||
openstack-placement>=10.0
|
openstack-placement>=10.0
|
||||||
shared-file-system =
|
shared-file-system =
|
||||||
manila>=18.0
|
manila>=18.0
|
||||||
|
baremetal =
|
||||||
|
ironic>=26.0
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
show_column_numbers = true
|
show_column_numbers = true
|
||||||
|
@@ -38,3 +38,7 @@ fi
|
|||||||
if [ -z "$1" -o "$1" = "dns" ]; then
|
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
|
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
|
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"
|
codegenerator_work_dir: "wrk"
|
||||||
install_additional_projects: []
|
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:
|
- job:
|
||||||
name: codegenerator-openapi-block-storage-tips
|
name: codegenerator-openapi-block-storage-tips
|
||||||
parent: codegenerator-openapi-tips-base
|
parent: codegenerator-openapi-tips-base
|
||||||
@@ -315,6 +344,8 @@
|
|||||||
description: |
|
description: |
|
||||||
Published OpenAPI specs
|
Published OpenAPI specs
|
||||||
dependencies:
|
dependencies:
|
||||||
|
- name: codegenerator-openapi-baremetal-tips-with-api-ref
|
||||||
|
soft: true
|
||||||
- name: codegenerator-openapi-block-storage-tips-with-api-ref
|
- name: codegenerator-openapi-block-storage-tips-with-api-ref
|
||||||
soft: true
|
soft: true
|
||||||
- name: codegenerator-openapi-compute-tips-with-api-ref
|
- name: codegenerator-openapi-compute-tips-with-api-ref
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
jobs:
|
jobs:
|
||||||
- openstack-tox-pep8
|
- openstack-tox-pep8
|
||||||
- openstack-tox-py311
|
- openstack-tox-py311
|
||||||
|
- codegenerator-openapi-baremetal-tips-with-api-ref
|
||||||
- codegenerator-openapi-block-storage-tips-with-api-ref
|
- codegenerator-openapi-block-storage-tips-with-api-ref
|
||||||
- codegenerator-openapi-compute-tips-with-api-ref
|
- codegenerator-openapi-compute-tips-with-api-ref
|
||||||
- codegenerator-openapi-dns-tips-with-api-ref
|
- codegenerator-openapi-dns-tips-with-api-ref
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
jobs:
|
jobs:
|
||||||
- openstack-tox-pep8
|
- openstack-tox-pep8
|
||||||
- openstack-tox-py311
|
- openstack-tox-py311
|
||||||
|
- codegenerator-openapi-baremetal-tips-with-api-ref
|
||||||
- codegenerator-openapi-block-storage-tips-with-api-ref
|
- codegenerator-openapi-block-storage-tips-with-api-ref
|
||||||
- codegenerator-openapi-compute-tips-with-api-ref
|
- codegenerator-openapi-compute-tips-with-api-ref
|
||||||
- codegenerator-openapi-dns-tips-with-api-ref
|
- codegenerator-openapi-dns-tips-with-api-ref
|
||||||
|
Reference in New Issue
Block a user