Skip to content
This repository has been archived by the owner on Apr 10, 2024. It is now read-only.

Commit

Permalink
feat: process network.quota and fip
Browse files Browse the repository at this point in the history
- split neutron schemas to separate file (is expected to grow)
- generate network.quota for rust
- generate few network.floatingip commands for rust
- add automatization for `find` operation in metadata/rust-sdk and
  rust-cli
  • Loading branch information
gtema committed Dec 19, 2023
1 parent da19bfc commit c92bc71
Show file tree
Hide file tree
Showing 14 changed files with 521 additions and 81 deletions.
7 changes: 1 addition & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ repos:
rev: v1.4.1
hooks:
- id: mypy
files: '^codegenerator/.*\.py$'
additional_dependencies:
- types-decorator
- types-PyYAML
# - types-requests
# - types-simplejson
# exclude: |
# (?x)(
# codegenerator/openapi/.*
# )
6 changes: 3 additions & 3 deletions codegenerator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,12 @@ def main():

openapi_spec = generator.get_openapi_spec(
Path(
metadata_path.parent,
op_data.spec_file or res_data.spec_file,
# metadata_path.parent,
op_data.spec_file
or res_data.spec_file,
).resolve()
)

# res_mods.append(
for mod_path, mod_name, path in generators[
args.target
].generate(
Expand Down
34 changes: 25 additions & 9 deletions codegenerator/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
from pathlib import Path
from typing import Any
import re

Expand All @@ -22,6 +23,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]
Expand Down Expand Up @@ -59,7 +63,7 @@ class BaseCompoundType(BaseModel):
description: str | None = None


def get_openapi_spec(path: str):
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))
Expand All @@ -77,7 +81,7 @@ def find_openapi_operation(spec, operationId: str):
raise RuntimeError("Cannot find operation %s specification" % operationId)


def get_plural_form(resource):
def get_plural_form(resource: str) -> str:
"""Get plural for of the resource"""
if resource[-1] == "y":
return resource[0:-1] + "ies"
Expand Down Expand Up @@ -108,8 +112,11 @@ def find_resource_schema(
raise RuntimeError("No type in %s" % schema)
schema_type = schema["type"]
if schema_type == "array":
print(f"plural {get_plural_form(resource_name)}")
if parent and parent == get_plural_form(resource_name):
if (
parent
and resource_name
and parent == get_plural_form(resource_name)
):
return (schema["items"], parent)
elif not parent and schema.get("items", {}).get("type") == "object":
# Array on the top level. Most likely we are searching for items
Expand All @@ -132,6 +139,12 @@ def find_resource_schema(
(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
return (props[keys[0]], keys[0])
return (None, None)


Expand All @@ -142,18 +155,21 @@ def get_resource_names_from_url(path: str):
path_elements.pop(0)
path_resource_names = []

# if len([x for x in all_paths if x.startswith(path + "/")]) > 0:
# has_subs = True
# else:
# has_subs = False
for path_element in path_elements:
if "{" not in path_element:
el = path_element.replace("-", "_")
if el[-3:] == "ies":
path_resource_names.append(el[0:-3] + "y")
elif el[-4:] == "sses":
path_resource_names.append(el[0:-2])
elif el[-1] == "s" and el[-3:] != "dns" and el[-6:] != "access":
elif (
el[-1] == "s"
and el[-3:] != "dns"
and el[-6:] != "access"
and el != "qos"
# quota/details
and el != "details"
):
path_resource_names.append(el[0:-1])
else:
path_resource_names.append(el)
Expand Down
112 changes: 103 additions & 9 deletions codegenerator/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import pathlib
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
import jsonref
from ruamel.yaml import YAML


class MetadataGenerator(BaseGenerator):
Expand All @@ -47,7 +47,11 @@ def generate(
# 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.load_openapi(pathlib.Path(args.openapi_yaml_spec))
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():
Expand All @@ -59,7 +63,7 @@ def generate(
f"{args.service_type}.{resource_name}",
ResourceModel(
api_version=api_ver,
spec_file=args.openapi_yaml_spec,
spec_file=spec_path.as_posix(),
operations=dict(),
),
)
Expand Down Expand Up @@ -93,6 +97,8 @@ def generate(
elif path.endswith("/detail"):
if method == "get":
operation_key = "list_detailed"
# elif path.endswith("/default"):
# operation_key = "default"
elif response_schema and (
response_schema.get("type", "") == "array"
or (
Expand Down Expand Up @@ -258,13 +264,97 @@ def generate(
# 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

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="fake",
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)
with open(
pathlib.Path(target_dir, args.service_type + "_metadata.yaml"), "w"
) as fp:
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
Expand All @@ -286,6 +376,8 @@ def get_operation_type_by_key(operation_key):
return "delete"
elif operation_key in ["create"]:
return "create"
elif operation_key == "default":
return "get"
else:
return "action"

Expand Down Expand Up @@ -346,4 +438,6 @@ def get_module_name(name):
return "delete"
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))
60 changes: 34 additions & 26 deletions codegenerator/openapi/neutron.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from codegenerator.common.schema import TypeSchema
from codegenerator.openapi.base import OpenStackServerSourceBase
from codegenerator.openapi.base import VERSION_RE
from codegenerator.openapi import neutron_schemas
from codegenerator.openapi.utils import merge_api_ref_doc


Expand Down Expand Up @@ -56,30 +57,6 @@
paste.app_factory = neutron.api.v2.router:APIRouter.factory
"""

EXTENSION_SCHEMA = {
"type": "object",
"properties": {
"alias": {
"type": "string",
"description": "A short name by which this extension is also known.",
},
"description": {
"type": "string",
"description": "Text describing this extension’s purpose.",
},
"name": {"type": "string", "description": "Name of the extension."},
"namespace": {
"type": "string",
"description": "A URL pointing to the namespace for this extension.",
},
"updated": {
"type": "string",
"format": "date-time",
"description": "The date and time when the resource was updated.",
},
},
}


class NeutronGenerator(OpenStackServerSourceBase):
URL_TAG_MAP = {
Expand Down Expand Up @@ -399,6 +376,14 @@ def _process_router(self, router, openapi_spec, processed_routes):
) and route.conditions["method"][0] in ["GET"]:
# There is no "show" for AZ
continue
if route.routepath in ["/quotas/tenant", "/quotas/project"]:
# Tenant and Project quota are not a thing
continue
if route.routepath == "/quotas" and route.conditions["method"][
0
] in ["POST"]:
# Tenant and Project quota is the same
continue

self._process_route(route, openapi_spec, processed_routes)

Expand Down Expand Up @@ -765,11 +750,13 @@ def _get_schema_ref(
schema.properties = {
"extensions": {
"type": "array",
"items": copy.deepcopy(EXTENSION_SCHEMA),
"items": copy.deepcopy(neutron_schemas.EXTENSION_SCHEMA),
}
}
elif name == "ExtensionShowResponse":
schema.properties = {"extension": copy.deepcopy(EXTENSION_SCHEMA)}
schema.properties = {
"extension": copy.deepcopy(neutron_schemas.EXTENSION_SCHEMA)
}
elif name.endswith("TagsIndexResponse"):
schema.properties = {
"tags": {
Expand All @@ -786,6 +773,27 @@ def _get_schema_ref(
"items": {"type": "string", "maxLength": 255},
}
}
elif name == "QuotasIndexResponse":
schema.properties = {
"quotas": {
"type": "array",
"items": copy.deepcopy(neutron_schemas.QUOTA_SCHEMA),
}
}
elif name == "QuotasDetailsDetailsResponse":
schema.properties = {
"quota": copy.deepcopy(neutron_schemas.QUOTA_DETAILS_SCHEMA)
}
elif name in [
"QuotaShowResponse",
"QuotaUpdateRequest",
"QuotaUpdateResponse",
"QuotasDefaultDefaultResponse",
"QuotasProjectProjectResponse",
]:
schema.properties = {
"quota": copy.deepcopy(neutron_schemas.QUOTA_SCHEMA)
}
elif name.endswith("TagUpdateRequest") or name.endswith(
"TagUpdateResponse"
):
Expand Down
Loading

0 comments on commit c92bc71

Please sign in to comment.