Enable generation of manila openapi

- Add Manila stub
- Add support for newer decorators with response info
- add new jobs for building spec

Change-Id: I3aa578af4ca50297ad0f3860f69e33df59ac6f22
This commit is contained in:
Artem Goncharov
2024-06-17 18:21:44 +02:00
parent 3ac8000fcb
commit 2e45767a49
7 changed files with 295 additions and 44 deletions

View File

@@ -17,7 +17,7 @@ import importlib
import inspect import inspect
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Callable
import re import re
from codegenerator.common.schema import ParameterSchema from codegenerator.common.schema import ParameterSchema
@@ -365,25 +365,28 @@ class OpenStackServerSourceBase:
for action, op_name in controller_actions.items(): for action, op_name in controller_actions.items():
logging.info("Action %s: %s", action, op_name) logging.info("Action %s: %s", action, op_name)
(start_version, end_version) = (None, None) (start_version, end_version) = (None, None)
action_impls: list[tuple[Callable, str | None, str | None]] = (
[]
)
if isinstance(op_name, str): if isinstance(op_name, str):
# wsgi action value is a string # wsgi action value is a string
if op_name in versioned_methods: if op_name in versioned_methods:
# ACTION with version bounds # ACTION with version bounds
if len(versioned_methods[op_name]) > 1:
raise RuntimeError(
"Multiple versioned methods for action %s",
action,
)
for ver_method in versioned_methods[op_name]: for ver_method in versioned_methods[op_name]:
start_version = ver_method.start_version action_impls.append(
end_version = ver_method.end_version (
func = ver_method.func ver_method.func,
logging.info("Versioned action %s", func) ver_method.start_version,
# operation_id += f"[{op_name}]" ver_method.end_version,
)
)
logging.info(
"Versioned action %s", ver_method.func
)
elif hasattr(contr, op_name): elif hasattr(contr, op_name):
# ACTION with no version bounds # ACTION with no version bounds
func = getattr(contr, op_name) func = getattr(contr, op_name)
# operation_id += f"[{op_name}]" action_impls.append((func, None, None))
logging.info("Unversioned action %s", func) logging.info("Unversioned action %s", func)
else: else:
logging.error( logging.error(
@@ -405,15 +408,20 @@ class OpenStackServerSourceBase:
if key and key in versioned_methods: if key and key in versioned_methods:
# ACTION with version bounds # ACTION with version bounds
if len(versioned_methods[key]) > 1: if len(versioned_methods[key]) > 1:
raise RuntimeError( logging.warn(
"Multiple versioned methods for action %s", f"There are multiple callables for action {key} instead of multiple bodies"
action,
) )
for ver_method in versioned_methods[key]: for ver_method in versioned_methods[key]:
start_version = ver_method.start_version action_impls.append(
end_version = ver_method.end_version (
func = ver_method.func ver_method.func,
logging.info("Versioned action %s", func) ver_method.start_version,
ver_method.end_version,
)
)
logging.info(
"Versioned action %s", ver_method.func
)
elif slf and key: elif slf and key:
vm = getattr(slf, "versioned_methods", None) vm = getattr(slf, "versioned_methods", None)
if vm and key in vm: if vm and key in vm:
@@ -424,12 +432,18 @@ class OpenStackServerSourceBase:
action, action,
) )
for ver_method in vm[key]: for ver_method in vm[key]:
start_version = ver_method.start_version action_impls.append(
end_version = ver_method.end_version (
func = ver_method.func ver_method.func,
logging.info("Versioned action %s", func) ver_method.start_version,
ver_method.end_version,
)
)
logging.info(
"Versioned action %s", ver_method.func
)
else: else:
func = op_name action_impls.append((op_name, None, None))
# Get the path/op spec only when we have # Get the path/op spec only when we have
# something to fill in # something to fill in
@@ -442,19 +456,20 @@ class OpenStackServerSourceBase:
operation_spec.tags.extend(operation_tags) operation_spec.tags.extend(operation_tags)
operation_spec.tags = list(set(operation_spec.tags)) operation_spec.tags = list(set(operation_spec.tags))
self.process_operation( for func, start_version, end_version in action_impls:
func, self.process_operation(
openapi_spec, func,
operation_spec, openapi_spec,
path_resource_names, operation_spec,
controller=controller, path_resource_names,
operation_name=action, controller=controller,
method=method, operation_name=action,
start_version=start_version, method=method,
end_version=end_version, start_version=start_version,
mode="action", end_version=end_version,
path=path, mode="action",
) path=path,
)
elif framework == "pecan": elif framework == "pecan":
if callable(controller): if callable(controller):
func = controller func = controller
@@ -534,6 +549,22 @@ class OpenStackServerSourceBase:
operation_name, operation_name,
func, func,
) )
# New decorators start having explicit null ApiVersion instead of being null
if (
start_version
and not isinstance(start_version, str)
and self._api_ver_major(start_version) in [0, None]
and self._api_ver_minor(start_version) in [0, None]
):
start_version = None
if (
end_version
and not isinstance(end_version, str)
and self._api_ver_major(end_version) in [0, None]
and self._api_ver_minor(end_version) in [0, None]
):
end_version = None
deser_schema = None deser_schema = None
deser = getattr(controller, "deserializer", None) deser = getattr(controller, "deserializer", None)
if deser: if deser:
@@ -583,8 +614,12 @@ class OpenStackServerSourceBase:
start_version.get_string() start_version.get_string()
) )
if mode != "action" and end_version: if (
if end_version.ver_major == 0: mode != "action"
and end_version
and self._api_ver_major(end_version)
):
if self._api_ver_major(end_version) == 0:
operation_spec.openstack.pop("max-ver", None) operation_spec.openstack.pop("max-ver", None)
operation_spec.deprecated = None operation_spec.deprecated = None
else: else:
@@ -618,7 +653,11 @@ class OpenStackServerSourceBase:
closure = inspect.getclosurevars(f) closure = inspect.getclosurevars(f)
closure_locals = closure.nonlocals closure_locals = closure.nonlocals
min_ver = closure_locals.get("min_version", start_version) min_ver = closure_locals.get("min_version", start_version)
if min_ver and not isinstance(min_ver, str):
min_ver = min_ver.get_string()
max_ver = closure_locals.get("max_version", end_version) max_ver = closure_locals.get("max_version", end_version)
if max_ver and not isinstance(max_ver, str):
max_ver = max_ver.get_string()
if "errors" in closure_locals: if "errors" in closure_locals:
expected_errors = closure_locals["errors"] expected_errors = closure_locals["errors"]
@@ -631,9 +670,14 @@ class OpenStackServerSourceBase:
] ]
elif isinstance(expected_errors, int): elif isinstance(expected_errors, int):
expected_errors = [str(expected_errors)] expected_errors = [str(expected_errors)]
if "request_body_schema" in closure_locals: if "request_body_schema" in closure_locals or hasattr(
f, "_request_body_schema"
):
# Body type is known through method decorator # Body type is known through method decorator
obj = closure_locals["request_body_schema"] obj = closure_locals.get(
"request_body_schema",
getattr(f, "_request_body_schema", {}),
)
if obj.get("type") in ["object", "array"]: if obj.get("type") in ["object", "array"]:
# We only allow object and array bodies # We only allow object and array bodies
# To prevent type name collision keep module name part of the name # To prevent type name collision keep module name part of the name
@@ -666,8 +710,22 @@ class OpenStackServerSourceBase:
ref_name = f"#/components/schemas/{typ_name}" ref_name = f"#/components/schemas/{typ_name}"
body_schemas.append(ref_name) body_schemas.append(ref_name)
if "query_params_schema" in closure_locals: if "response_body_schema" in closure_locals or hasattr(
obj = closure_locals["query_params_schema"] f, "_response_body_schema"
):
# Response type is known through method decorator - PERFECT
obj = closure_locals.get(
"response_body_schema",
getattr(f, "_response_body_schema", {}),
)
ser_schema = obj
if "query_params_schema" in closure_locals or hasattr(
f, "_request_query_schema"
):
obj = closure_locals.get(
"query_params_schema",
getattr(f, "_request_query_schema", {}),
)
query_params_versions.append((obj, min_ver, max_ver)) query_params_versions.append((obj, min_ver, max_ver))
f = f.__wrapped__ f = f.__wrapped__
@@ -704,7 +762,9 @@ class OpenStackServerSourceBase:
if query_params_versions: if query_params_versions:
so = sorted( so = sorted(
query_params_versions, query_params_versions,
key=lambda d: d[1].split(".") if d[1] else (0, 0), key=lambda d: (
tuple(map(int, d[1].split("."))) if d[1] else (0, 0)
),
) )
for data, min_ver, max_ver in so: for data, min_ver, max_ver in so:
self.process_query_parameters( self.process_query_parameters(

View File

@@ -0,0 +1,145 @@
# 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 (
SpecSchema,
)
from codegenerator.openapi.base import OpenStackServerSourceBase
from codegenerator.openapi.utils import merge_api_ref_doc
class ManilaGenerator(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):
import fixtures
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_concurrency import lockutils
from manila.api.openstack import api_version_request
from manila.api.v2 import router
from manila import rpc
# from placement import handler
from manila.common import config
from manila import coordination
self.api_version = api_version_request._MAX_API_VERSION
self.min_api_version = api_version_request._MIN_API_VERSION
CONF = config.CONF
lock_path = self.useFixture(fixtures.TempDir()).path
self.fixture = self.useFixture(config_fixture.Config(lockutils.CONF))
self.fixture.config(lock_path=lock_path, group="oslo_concurrency")
self.fixture.config(
disable_process_locking=True, group="oslo_concurrency"
)
rpc.init(CONF)
CONF.set_override(
"backend_url", "file://" + lock_path, group="coordination"
)
coordination.LOCK_COORDINATOR.start()
# config = cfg.ConfigOpts()
# conf_fixture = self.useFixture(config_fixture.Config(config))
# conf.register_opts(conf_fixture.conf)
# handler = handler.PlacementHandler(config=conf_fixture.conf)
self.router = router.APIRouter()
work_dir = Path(target_dir)
work_dir.mkdir(parents=True, exist_ok=True)
impl_path = Path(
work_dir,
"openapi_specs",
"shared_file_system",
f"v{self.api_version}.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 Shared-File-System API",
description=LiteralScalarString(
"Shared File System API provided by Manila service"
),
version=self.api_version,
),
openapi="3.1.0",
security=[{"ApiKeyAuth": []}],
components=dict(
securitySchemes={
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-Auth-Token",
}
},
),
)
for route in self.router.map.matchlist:
if route.routepath.endswith(".:(format)"):
continue
if route.routepath.startswith("/{project"):
continue
# if not route.routepath.startswith("/resource-lock"):
# 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)
lnk = Path(impl_path.parent, "v2.yaml")
lnk.unlink(missing_ok=True)
lnk.symlink_to(impl_path.name)
return impl_path
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 Manila OpenAPI schema")

View File

@@ -63,6 +63,11 @@ class OpenApiSchemaGenerator(BaseGenerator):
PlacementGenerator().generate(target_dir, args) PlacementGenerator().generate(target_dir, args)
def generate_manila(self, target_dir, args):
from codegenerator.openapi.manila import ManilaGenerator
ManilaGenerator().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
): ):
@@ -85,6 +90,8 @@ class OpenApiSchemaGenerator(BaseGenerator):
self.generate_neutron(target_dir, args) self.generate_neutron(target_dir, args)
elif args.service_type == "placement": elif args.service_type == "placement":
self.generate_placement(target_dir, args) self.generate_placement(target_dir, args)
elif args.service_type == "shared-file-system":
self.generate_manila(target_dir, args)
else: else:
raise RuntimeError( raise RuntimeError(
"Service type %s is not supported", args.service_type "Service type %s is not supported", args.service_type

View File

@@ -43,6 +43,8 @@ network =
neutron-vpnaas>=23.0 neutron-vpnaas>=23.0
placement = placement =
openstack-placement>=10.0 openstack-placement>=10.0
shared-file-system =
manila>=18.0
[mypy] [mypy]
show_column_numbers = true show_column_numbers = true

View File

@@ -30,3 +30,7 @@ if [ -z "$1" -o "$1" = "placement" ]; then
openstack-codegenerator --work-dir wrk --target openapi-spec --service-type placement --api-ref-src ${API_REF_BUILD_ROOT}/placement/api-ref/build/html/index.html --validate openstack-codegenerator --work-dir wrk --target openapi-spec --service-type placement --api-ref-src ${API_REF_BUILD_ROOT}/placement/api-ref/build/html/index.html --validate
sed -i "s/(?expanded=delete-resource-provider-inventories-detail#delete-resource-provider-inventories)//" wrk/openapi_specs/placement/v1.yaml sed -i "s/(?expanded=delete-resource-provider-inventories-detail#delete-resource-provider-inventories)//" wrk/openapi_specs/placement/v1.yaml
fi fi
if [ -z "$1" -o "$1" = "shared-file-system" ]; then
openstack-codegenerator --work-dir wrk --target openapi-spec --service-type shared-file-system --api-ref-src ${API_REF_BUILD_ROOT}/manila/api-ref/build/html/index.html --validate
fi

View File

@@ -239,6 +239,35 @@
project: "opendev.org/openstack/placement" project: "opendev.org/openstack/placement"
path: "/api-ref/build/html/index.html" path: "/api-ref/build/html/index.html"
- job:
name: codegenerator-openapi-shared-file-system-tips
parent: codegenerator-openapi-tips-base
description: |
Generate OpenAPI spec for Manila
required-projects:
- name: openstack/manila
vars:
openapi_service: shared-file-system
install_additional_projects:
- project: "opendev.org/openstack/manila"
name: "."
- job:
name: codegenerator-openapi-shared-file-system-tips-with-api-ref
parent: codegenerator-openapi-shared-file-system-tips
description: |
Generate OpenAPI spec for Manila consuming API-REF
required-projects:
- name: openstack/manila
pre-run:
- playbooks/openapi/pre-api-ref.yaml
vars:
codegenerator_api_ref:
project: "opendev.org/openstack/manila"
path: "/api-ref/build/html/index.html"
- job: - job:
name: codegenerator-tox-publish-openapi-specs name: codegenerator-tox-publish-openapi-specs
parent: opendev-tox-docs parent: opendev-tox-docs
@@ -259,6 +288,8 @@
soft: true soft: true
- name: codegenerator-openapi-placement-tips-with-api-ref - name: codegenerator-openapi-placement-tips-with-api-ref
soft: true soft: true
- name: codegenerator-openapi-shared-file-system-tips-with-api-ref
soft: true
pre-run: pre-run:
- playbooks/openapi/fetch.yaml - playbooks/openapi/fetch.yaml
vars: vars:

View File

@@ -13,6 +13,7 @@
- codegenerator-openapi-load-balancing-tips-with-api-ref - codegenerator-openapi-load-balancing-tips-with-api-ref
- codegenerator-openapi-network-tips-with-api-ref - codegenerator-openapi-network-tips-with-api-ref
- codegenerator-openapi-placement-tips-with-api-ref - codegenerator-openapi-placement-tips-with-api-ref
- codegenerator-openapi-shared-file-system-tips-with-api-ref
- codegenerator-tox-publish-openapi-specs - codegenerator-tox-publish-openapi-specs
gate: gate:
jobs: jobs:
@@ -25,4 +26,5 @@
- codegenerator-openapi-load-balancing-tips-with-api-ref - codegenerator-openapi-load-balancing-tips-with-api-ref
- codegenerator-openapi-network-tips-with-api-ref - codegenerator-openapi-network-tips-with-api-ref
- codegenerator-openapi-placement-tips-with-api-ref - codegenerator-openapi-placement-tips-with-api-ref
- codegenerator-openapi-shared-file-system-tips-with-api-ref
- codegenerator-tox-publish-openapi-specs - codegenerator-tox-publish-openapi-specs