Slide 1

Slide 1 text

Steps down the OpenAPI Path An up-to-date history of OpenAPI in OpenStack OpenInfra Summit Europe ‘25 @stephenfin (Stephen Finucane) @gtema (Artem Goncharov)

Slide 2

Slide 2 text

Stephen Finucane (@stephenfin) Principal Software Engineer Red Hat

Slide 3

Slide 3 text

Artem Goncharov (@gtema) System Architect - IAM SysEleven GmbH

Slide 4

Slide 4 text

Background

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

OpenStackSDK (Python) ↗ Gophercloud (Go) ↗ openstack_sdk (Rust) ↗ OpenStack4j (Java) ↗ php-opencloud/openstack (PHP) ↗ …

Slide 7

Slide 7 text

Writing & Maintaining SDKs is hard work There are so many services There are so many APIs 🎶 A Good Song Bad API Never Dies 🎶 …

Slide 8

Slide 8 text

Other issues OpenStackClient (and SDK) can be slooowwww… 🦀 Completeness of existing SDKs New languages, new environments Obsolete web frameworks and libraries

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Superset of JSONSchema (draft 2020-12) Support for extensions Schemas given in JSON or YAML Support in multiple languages (validation, doc generation, …)

Slide 11

Slide 11 text

openapi: "3.1.0" info: version: "1.0.0" title: "Swagger Petstore" license: name: "MIT" servers: - url: "http://petstore.swagger.io/v1" paths: /pets: get: # ... petstore.yaml

Slide 12

Slide 12 text

# ... paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer maximum: 100 format: int32 # ... petstore.yaml

Slide 13

Slide 13 text

# ... paths: /pets: get: # ... responses: '200': description: A paged array of pets headers: x-next: description: A link to the next page of responses schema: type: string content: application/json: schema: $ref: "#/components/schemas/Pets" # ... petstore.yaml

Slide 14

Slide 14 text

FAQs

Slide 15

Slide 15 text

Q: Who needs OpenAPI when we have api-ref?

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

A: Everyone*

Slide 18

Slide 18 text

Problems api-ref documentation is not machine-readable api-ref documentation can be incomplete/outdated api-ref is reStructuredText Verification is difficult Schemas live in another project

Slide 19

Slide 19 text

Q: Haven’t we tried this before?

Slide 20

Slide 20 text

Q: Haven’t we tried this before? A: Yes ↗. Many ↗ times ↗. However!

Slide 21

Slide 21 text

Superset of JSONSchema (draft 2020-12) Support for extensions Schemas given in JSON or YAML Support in multiple languages (validation, doc generation, …)

Slide 22

Slide 22 text

Q: Why OpenAPI over RAML, API Blueprint etc.?

Slide 23

Slide 23 text

Superset of JSONSchema (draft 2020-12) Support for extensions Schemas given in JSON or YAML Support in multiple languages (validation, doc generation, …)

Slide 24

Slide 24 text

Superset of JSONSchema (draft 2020-12) Support for extensions 🌟🌟 Tooling support 🌟🌟 Schemas given in JSON or YAML Support in multiple languages (validation, doc generation, …)

Slide 25

Slide 25 text

How We Do It

Slide 26

Slide 26 text

Some OpenAPI background… Schema-derived apps vs. app-derived schemas

Slide 27

Slide 27 text

Some OpenAPI background… Schema-derived apps vs. app-derived schemas Extensibility

Slide 28

Slide 28 text

Some OpenStack background… Extensive use of Paste, WebOb, Routes

Slide 29

Slide 29 text

Some OpenStack background… Extensive use of Paste, WebOb, Routes …but not exclusive use

Slide 30

Slide 30 text

class FlavorAccessController(wsgi.Controller): """The flavor access API controller for the OpenStack API.""" @wsgi.expected_errors(404) @validation.query_schema(schema.index_query) def index(self, req, flavor_id): context = req.environ['nova.context'] context.can(fa_policies.BASE_POLICY_NAME) flavor = common.get_flavor(context, flavor_id) # public flavor to all projects if flavor.is_public: explanation = _("...") raise webob.exc.HTTPNotFound(explanation=explanation) # private flavor to listed projects only return _marshall_flavor_access(flavor) nova/api/openstack/compute/flavor_access.py

Slide 31

Slide 31 text

class FlavorAccessController(wsgi.Controller): """The flavor access API controller for the OpenStack API.""" @wsgi.expected_errors(404) @validation.query_schema(schema.index_query) def index(self, req, flavor_id): context = req.environ['nova.context'] context.can(fa_policies.BASE_POLICY_NAME) flavor = common.get_flavor(context, flavor_id) # public flavor to all projects if flavor.is_public: explanation = _("...") raise webob.exc.HTTPNotFound(explanation=explanation) # private flavor to listed projects only return _marshall_flavor_access(flavor) nova/api/openstack/compute/flavor_access.py

Slide 32

Slide 32 text

class FlavorAccessController(wsgi.Controller): """The flavor access API controller for the OpenStack API.""" @wsgi.expected_errors(404) @validation.query_schema(schema.index_query) @validation.response_body_schema(schema.index_response) def index(self, req, flavor_id): context = req.environ['nova.context'] context.can(fa_policies.BASE_POLICY_NAME) flavor = common.get_flavor(context, flavor_id) # public flavor to all projects if flavor.is_public: explanation = _("...") raise webob.exc.HTTPNotFound(explanation=explanation) # private flavor to listed projects only return _marshall_flavor_access(flavor) nova/api/openstack/compute/flavor_access.py

Slide 33

Slide 33 text

from nova.api.validation import parameter_types # ... index_response = { 'type': 'object', 'properties': { # NOTE(stephenfin): While we accept numbers as values, we always # return strings 'extra_specs': parameter_types.metadata, }, 'required': ['extra_specs'], 'additionalProperties': False, } # ... nova/api/openstack/compute/schemas/flavor_access.py

Slide 34

Slide 34 text

@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION) @wsgi.Controller.authorize('get_all') @validation.request_query_schema(schema.index_request_query) @validation.response_body_schema(schema.index_response_body) def index(self, req): """Returns a list of locks, transformed through view builder.""" context = req.environ['manila.context'] filters = req.params.copy() params = common.get_pagination_params(req) limit, offset = [ params.pop('limit', None), params.pop('offset', None) ] sort_key, sort_dir = common.get_sort_params(filters) for key in ('limit', 'offset'): filters.pop(key, None) # ... manila/api/v2/resource_locks.py

Slide 35

Slide 35 text

@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION) @wsgi.Controller.authorize('get_all') @validation.request_query_schema(schema.index_request_query) @validation.response_body_schema(schema.index_response_body) def index(self, req): """Returns a list of locks, transformed through view builder.""" context = req.environ['manila.context'] filters = req.params.copy() params = common.get_pagination_params(req) limit, offset = [ params.pop('limit', None), params.pop('offset', None) ] sort_key, sort_dir = common.get_sort_params(filters) for key in ('limit', 'offset'): filters.pop(key, None) # ... manila/api/v2/resource_locks.py

Slide 36

Slide 36 text

from manila.api.validation import helpers create_request_body = { 'type': 'object', 'properties': { 'resource_lock': { 'type': 'object', 'properties': { 'resource_id': { 'type': 'string', 'format': 'uuid', 'description': helpers.description( 'resource_lock_resource_id' ), }, }, }, }, } manila/api/schemas/resource_locks.py

Slide 37

Slide 37 text

from manila.api.validation import helpers create_request_body = { 'type': 'object', 'properties': { 'resource_lock': { 'type': 'object', 'properties': { 'resource_id': { 'type': 'string', 'format': 'uuid', 'description': helpers.description( 'resource_lock_resource_id' ), }, }, }, }, } manila/api/schemas/resource_locks.py

Slide 38

Slide 38 text

❯ tox -e py310 functional-py310

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

What Then?

Slide 41

Slide 41 text

Nova ✅⭐ Keystone ✅⭐ Manila 📝⭐ Ironic 📝⭐ Cinder 📝⭐ Neutron ✅* Designate 📝 Octavia ✅* Magnum 📝 Swift ✅* Glance 📝 Barbican 📝

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

What does it do?

Slide 44

Slide 44 text

What does it do? Generates OpenAPI schemas Generate bindings in chosen language

Slide 45

Slide 45 text

# ... paths: /v2.1/flavors/{flavor_id}/os-flavor-access: parameters: - $ref: '#/components/parameters/flavors_os_flavor_access_flavor_id' get: operationId: flavors/flavor_id/os-flavor-access:get responses: '404': description: Error '200': description: Ok content: application/json: schema: $ref: '#/components/schemas/FlavorsOs_Flavor_AccessListResponse' openapi_specs/compute/v2.96.yaml (generated file)

Slide 46

Slide 46 text

# ... components: schemas: FlavorsOs_Flavor_AccessListResponse: type: object properties: flavor_access: type: array items: type: object properties: flavor_id: type: string format: uuid tenant_id: type: string format: uuid openapi_specs/compute/v2.96.yaml (generated file with field omitted)

Slide 47

Slide 47 text

What does it do? Generates OpenAPI schemas Generate bindings in chosen language Generates CLI and TUI (and SDK)

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

How does it do it?

Slide 52

Slide 52 text

How does it do it? Long live the metadata

Slide 53

Slide 53 text

resources: network.security_group_rule: api_version: v2 operations: create: operation_id: security-group-rules:post operation_type: create targets: rust-cli: cli_full_command: security-group-rule create module_name: create sdk_mod_name: create rust-sdk: module_name: create rust-tui: module_name: create ... spec_file: wrk/openapi_specs/network/v2.yaml codegenerator/src/metadata/network_metadata.yaml

Slide 54

Slide 54 text

Recipe: 🔪 OpenAPI converted to ADT 🥗ADT converted to target specific types (pydantic 🫣) ♨ Jinja2 templates 󰞫🎛 => 🥧 7k+ files

Slide 55

Slide 55 text

Perfect pipeline: API change is merged PR/Change automatically proposed to SDK/Cli/TUI

Slide 56

Slide 56 text

Next Steps

Slide 57

Slide 57 text

Get final Nova patches merged in Gazpacho Continue with Ironic, Cinder etc. Flesh out OpenAPI Sphinx extension Prepare for OpenAPI Moonwalk (4.0)

Slide 58

Slide 58 text

Want to know more? topic:openapi on review.opendev.org ↗

Slide 59

Slide 59 text

Want to get involved? #openstack-sdk on OFTC IRC ↗ [email protected] ↗ Catch us in person 🙋 (We don’t bite)

Slide 60

Slide 60 text

Steps down the OpenAPI Path An up-to-date history of OpenAPI in OpenStack OpenInfra Summit Europe ‘25 @stephenfin (Stephen Finucane) @gtema (Artem Goncharov)

Slide 61

Slide 61 text

Credits Cover photo by Aleksandra Boguslawska on Unsplash Source code from Nova and Manila projects (Apache 2.0)