OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

ec2-api — Rewrite pack

Rewrite pack — ec2-api — branch 17.0.0

Branch 17.0.0 @ 32740781f605 — repository https://opendev.org/openstack/ec2-api (retired; local branch pre-retirement) — analysed on 2026-10-29

Target: openkeel-ec2-api (Go), see generate-go-from-spec/references/backends/ec2-api.md. Other repositories cited: keystone: = openstack/keystone stable/2026.2 @ e6dc5ae07; keystoneclient: = python-keystoneclient stable/2026.2 @ b76de6a3. Extraction environment: Python 3.10, requirements constrained by openstack/requirements tag 2023.2-eol (SQLAlchemy 1.4.41, sqlalchemy-migrate, botocore 1.29.118).

Clients that must keep working (asked on 2026-10-29): AWS CLI / boto3 (botocore), Terraform AWS provider (aws-sdk-go), euca2ools / boto2 (SigV2), instance metadata consumers (cloud-init and others).

1. Index of _rewrite/

FileContentUse in the rewrite
[ec2-actions_17.0.0.json](_rewrite/ec2-actions_17.0.0.json)97 actions (wire name, controller and implementation file:line, parameters with validator type, required flag, default), 21 describers with their filter maps and sort keys, 87 exception classes (EC2 code, HTTP status, template), 15 dispatcher probesthe action table and the validators; the filter names; the error catalogue; the probes as regression tests of the Python behaviour
[actions_17.0.0.md](_rewrite/actions_17.0.0.md)the same actions as a tablereading
[action-translation_17.0.0.md](_rewrite/action-translation_17.0.0.md)OpenStack and DB calls each action made in the unit testscross-check of 01 § 2 and 03
[exchanges-api-tests_17.0.0.json](_rewrite/exchanges-api-tests_17.0.0.json)1009 Query API exchanges from 396 unit tests: form request, XML response parsed to a tree, status, and backend_calls (955 exchanges have some)golden request → response pairs; request → expected OpenStack calls
[exchanges-metadata-tests_17.0.0.json](_rewrite/exchanges-metadata-tests_17.0.0.json), [exchanges-s3-tests_17.0.0.json](_rewrite/exchanges-s3-tests_17.0.0.json), [exchanges-auth-tests_17.0.0.json](_rewrite/exchanges-auth-tests_17.0.0.json)33 metadata, 12 S3 (real botocore client), 7 auth-middleware exchangessame, for the other services
response-schemas-*-tests_17.0.0.{json,md}response schemas inferred per action and statusXML element names and nesting per action
observed-errors-*-tests_17.0.0.md(action, status, code, message) seen in the testserror conformance
[botocore-compat_17.0.0.{json,md}](_rewrite/botocore-compat_17.0.0.md)per action: SDK parameters ec2-api rejects with 500, required-vs-optional mismatches; 726 recorded responses parsed with botocore's EC2 parser (10 failures, all from test fakes with empty timestamps)client contract for boto3/AWS CLI/aws-sdk-go
[schema-sqlite-migrated_17.0.0.sql](_rewrite/schema-sqlite-migrated_17.0.0.sql)schema after ec2-api-manage db_sync on SQLitethe production schema
schema-mysql_17.0.0.sql, schema-postgresql_17.0.0.sqlDDL generated from the modelsMariaDB DDL
[schema-diff_17.0.0.md](_rewrite/schema-diff_17.0.0.md)models vs migration—
[config_17.0.0.conf](_rewrite/config_17.0.0.conf)oslo-config-generator sample (ec2api, ec2api.api, ec2api.metadata, ec2api.s3, keystoneauth1, oslo.*)option names and defaults (my_ip replaced by a placeholder)
tools/ec2_actions.py, tools/botocore_check.py, tools/render_tables.pythe extractors (need a venv with ec2-api; render_tables.py is stdlib)regenerate
[rewrite-pack-index_17.0.0.md](_rewrite/rewrite-pack-index_17.0.0.md)how each file was produced—

There are no policies (§ 6), no OpenAPI (the Query API is not REST), no microversions and no gabbi suite.

2. Cross-cutting HTTP behaviour

  1. One endpoint: POST / (and GET /?…) with Action and Version; responses text/xml (01 § 1). Middleware order decides which error wins (etc/ec2api/api-paste.ini:10): authentication (Keystone) → timestamp → action lookup → validation → implementation. An unknown action is reported only after a successful Keystone authentication.
  2. Error bodies: EC2 XML (ec2api/api/faults.py:35) for everything from the authentication filter on, except Requestify's webob plain-text 400/403 and FaultWrapper's plain 400 for SigV4 clients (02 § 1). All EC2 errors are HTTP 400, including *.NotFound; authentication failures carry Keystone's status (401/403/404/503).
  3. Version: never checked; echoed as the response namespace http://ec2.amazonaws.com/doc/<Version>/ (ec2api/api/apirequest.py:87). botocore sends 2016-11-15.
  4. Unknown parameters: 500 TypeError (01 § 1.3 step 8). This is the largest behavioural trap. A Go rewrite should ignore unknown parameters (AWS rejects them with UnknownParameter, but ignoring is what keeps SDK defaults working) and reject DryRun=true with UnsupportedOperation or answer DryRunOperation; keep the Python behaviour only behind a compatibility flag if bug-for-bug conformance is wanted. Decide explicitly; the probes in ec2-actions_17.0.0.json show the Python behaviour.
  5. Value coercion (ec2api/api/ec2utils.py:52): reproduce the type the implementation needs per parameter instead of guessing from the string (tag values, names and descriptions must stay strings; '0755' must not become 493). The Python behaviour is a source of 500s (numeric tag values) and wrong validation messages.
  6. Request ID: req-<uuid4> (ec2api/api/__init__.py:164), in requestId (success) or RequestID (error).
  7. No content negotiation, no Content-Length limits beyond eventlet's URL limit (16384, ec2api/cmd/api.py:34) and max_header_line (config_17.0.0.conf).
  8. Access log: one INFO line per request with action and status (ec2api/api/__init__.py:96).

3. Formal specification of the core algorithms

Algorithm 1 — EC2 ID mapping (must be kept bit-for-bit)

Existing databases, scripts and Terraform state hold these IDs; a rewrite that generates different IDs for existing objects breaks them.

Storage: table items (03 § 1.1). os_id is unique across all kinds and projects (ec2api/db/sqlalchemy/models.py:45).

Generation (ec2api/db/sqlalchemy/api.py:91): id = kind + "-" + format(randint(1, 0xffffffff), "08x") — lower-case hex, exactly 8 digits, never 00000000. On a duplicate id the insert fails (no retry). Use a CSPRNG in Go and retry on primary-key collision: IDs are opaque to clients, only their format matters.

Kinds and prefixes: i, ami, aki, ari, vol, snap, vpc, subnet, eni, sg, eipalloc, rtb, igw, dopt, vgw, cgw, vpn (03 § 1.2). The kind of an ID is the text before the first - (ec2api/api/ec2utils.py:213), its hex part the text after the last - (ec2api/api/ec2utils.py:217); validation splits on the last - (ec2api/api/validator.py:138) so eni-attach-1a2b3c4d validates as prefix eni-attach.

Derived IDs (never stored; same 8 hex digits as their source, ec2api/api/ec2utils.py:217):

IDDerived fromSource
sg-<h> default security group of a VPCvpc-<h>; the item is inserted with this exact ID (restore_item)ec2api/api/security_group.py:106
rtbassoc-<h> explicit associationsubnet-<h>ec2api/api/route_table.py:165
rtbassoc-<h> main associationvpc-<h>ec2api/api/route_table.py:522
eni-attach-<h>eni-<h>ec2api/api/network_interface.py:470
eipassoc-<h>eipalloc-<h>ec2api/api/address.py:384

ReplaceRouteTableAssociation returns the association ID it was given (ec2api/api/route_table.py:206): associations keep their ID when re-pointed.

Reservation IDs (ec2api/api/instance.py:1592): r- + 8 characters drawn from '01234567890abcdefghijklmnopqrstuvwxyz' (the digit 0 appears twice in the alphabet, so it is twice as likely); stored in the instance's data.reservation_id. Instances auto-created from Nova take Nova's OS-EXT-SRV-ATTR:reservation_id and launch_index (ec2api/api/instance.py:1474), which have the same r- format.

OpenStack → EC2 lookups:

os_id_to_ec2_id(kind, os_id, project_id=None):          # ec2api/api/ec2utils.py:335
    rows = SELECT id FROM items WHERE id LIKE kind||'-%' AND os_id = :os_id   -- any project
    if rows: return rows[0].id
    INSERT items(id=new_id(kind), os_id, project_id)   -- project_id NULL unless given
    on duplicate os_id: return (SELECT id FROM items WHERE os_id = :os_id).id  -- any kind!
get_db_item_by_os_id(kind, os_id):                      # ec2api/api/ec2utils.py:296
    item = first of get_items(kind) [caller's project] with item.os_id == os_id
    if none: add_item(kind, {os_id} + kind extension)   -- may *adopt* a NULL-project row
add_item(kind, data):                                   # ec2api/db/sqlalchemy/api.py:98
    INSERT with project_id = caller
    on duplicate os_id: row = the row with that os_id AND (project_id = caller OR NULL) AND kind
                        merge data into it; project_id = caller; UPDATE

Rules a rewrite must keep:

  1. An OpenStack object has at most one EC2 ID, for all projects (public images, other projects' snapshots referenced in image mappings get an ID-only row with NULL project, ec2api/api/image.py:424, ec2api/api/image.py:770).
  2. Such a row is adopted (project set) when the owning project later describes the object (ec2api/db/sqlalchemy/api.py:111); the ID does not change.
  3. Items are only visible to their project (project_id = caller) except through the ID lookups above and get_public_items.
  4. Cross-kind collisions: _format_volume_mapping maps a volume UUID with kind vol and puts it in snapshotId (ec2api/api/image.py:776); add_item_id on a duplicate os_id returns the existing ID whatever its kind (ec2api/db/sqlalchemy/api.py:140).
  5. OpenStack objects are named with their EC2 ID right after creation: Nova server name (ec2api/api/instance.py:170), Neutron router (ec2api/api/vpc.py:146), network and subnet (ec2api/api/subnet.py:84), port (ec2api/api/network_interface.py:158), Cinder volume and snapshot display_name (ec2api/api/volume.py:70, ec2api/api/snapshot.py:49), IKE/IPsec policies (ec2api/api/vpn_connection.py:100), VPN services (ec2api/api/vpn_gateway.py:176) and site connections vpn-…/subnet-… (ec2api/api/vpn_connection.py:414); the default security group of a VPC is the Neutron group **named vpc-<h>** (ec2api/api/security_group.py:118), which _translate_group_name relies on (ec2api/api/security_group.py:410); images made by CreateImage carry the Glance property ec2_id (ec2api/api/image.py:155). Keep writing these names: operators and the EC2-Classic network selection (subnet- prefix excluded, ec2api/api/instance.py:1455) depend on them.
  6. The data JSON must stay readable by Python ec2-api during a migration: Python json.dumps default separators (", ", ": "), true/false/null, keys as in 03 § 1.2. In particular get_public_items matches the substring "is_public": True case-insensitively (ec2api/db/sqlalchemy/api.py:228): a Go writer that emits "is_public":true hides public images from Python readers. A Go reader should parse the JSON instead of using LIKE.

Algorithm 2 — Query parameter decoding

ec2api/api/ec2utils.py:93, ec2api/api/apirequest.py:58:

args = {}
for (key, value) in form ∪ query (minus auth/system parameters, 01 § 1.3):
    parts = key.split(".")
    value = try_convert(value)                       # see 01 § 1.3 step 5
    walk/create nested dicts along snake(parts[0..n-2]); set snake(parts[n-1]) = value
collapse(args): for each key k with dict value d (non-empty):
    if first key of d is numeric: args[k] = [collapse(d[i]) for i in sorted(d) (string order)]
    elif d has the single key 'value': args[k] = d['value']
call implementation(context, **args)

snake() is ec2api/api/ec2utils.py:48 ((?<=[a-z])[A-Z] or [A-Z](?![A-Z]|$) prefixed with _, lower-cased, stripped). Note that "first key numeric" looks only at the first inserted key, and that string sorting puts .10 before .2 — lists of ten or more entries are reordered (ec2api/api/apirequest.py:72). A Go implementation should parse per action into typed structures (the parameter lists are in ec2-actions_17.0.0.json), sort list indexes numerically, and keep the Attribute.Value collapse for SourceDestCheck.Value, Description.Value, DisableApiTermination.Value, InstanceType.Value, UserData.Value…

Algorithm 3 — Block device mappings of RunInstances

ec2api/api/instance.py:890 (parse), ec2api/api/instance.py:944 (merge), ec2api/api/ec2utils.py:398 (image mappings):

  1. Parse BlockDeviceMapping.N: DeviceName, Ebs.SnapshotId (snap- → source_type=snapshot, vol- → source_type=volume (OpenStack extension), else InvalidSnapshotID.Malformed), Ebs.VolumeSize, Ebs.DeleteOnTermination; destination_type=volume. VirtualName and NoDevice are ignored. The same device given twice with different /dev/ prefixes → InvalidBlockDeviceMapping; identical → the last wins.
  2. Image mappings: virtual mappings (swap, ephemeralN) become blank local BDMs; block_device_mapping is v2 when bdm_v2 is set, else legacy and converted; delete_on_termination defaults to false; same device → replaced.
  3. Root device = image root_device_name, else the root entry of mappings (ec2api/api/ec2utils.py:520).
  4. For each requested BDM: not in the image → defaults delete_on_termination=True, source_type=blank, boot_index 0 if it is the root device else −1. In the image: the device name must match exactly (/dev/vda vs vda → error); the root snapshot cannot be changed; a smaller volume_size than the image's → error ("Volume of size …"); a new snapshot_id is allowed only on EBS image devices; otherwise the image BDM (without None values) is updated with the request.
  5. <source_type>_id is moved to uuid (ec2api/api/instance.py:1010). Only the requested BDMs are passed (Nova merges with the image's own).

Algorithm 4 — Instance state

ec2api/api/instance.py:1518, ec2api/api/instance.py:1673, ec2api/api/instance.py:1704: name = map(Nova OS-EXT-STS:vm_state), code = map(name), unknown names → code 0.

Nova vm_stateEC2 namecode
None, buildingpending0
activerunning16
deleted, soft-deleteshutting-down32
(gone from Nova) wiped_outterminated48
stoppedstopped80
pausedpause80
suspendedsuspend80
rescuedrescue16
resizedresize16
error, shelved, shelved_offloaded, anything elsethe vm_state itself (error…)0

stopping (64) is never produced: task states are not read. botocore accepts any name string.

Algorithm 5 — Route tables realised in Neutron

For a route table rtb and each subnet using it (explicitly, or implicitly when rtb is the VPC's main table) (ec2api/api/route_table.py:539):

destinations = items with id in (route gateway_ids ∪ network_interface_ids ∪ propagating_gateways)
               whose vpc_id == rtb.vpc_id, and, for vgw-, that have a VPN connection   # :585
gateway_ip = first address of the subnet CIDR + 1                                     # :608
nexthop(route) = gateway_ip           if local route or gateway in destinations
               = eni.private_ip_address if ENI route and ENI in destinations
               = 127.0.0.1            otherwise (blackhole)
host_routes = [ {destination: cidr, nexthop: nexthop(r)} for r in rtb.routes,
                skipping 0.0.0.0/0 when its nexthop is 127.0.0.1 ]
            + [ {destination: c, nexthop: gateway_ip} for each propagating vgw, c in its VPN cidrs ]
if no 0.0.0.0/0 route went to gateway_ip:
    host_routes += {destination: 169.254.169.254/32, nexthop: gateway_ip}; gateway_ip = None
neutron.update_subnet(subnet, {host_routes, gateway_ip})        # rollback restores the old values

(ec2api/api/route_table.py:604). So a subnet without a default route to an internet or VPN gateway has no Neutron gateway IP (instances get no default route), and the metadata address is routed through the router. The local route always appears (nexthop = gateway_ip) [inferred: Neutron ignores a host route equal to the subnet itself]. VPN: each VPN connection of the attached VPN gateway gets, per subnet, an ipsec_site_connection whose peer_cidrs = the table's static routes to the VPN gateway ∪ (the connection's cidrs if propagation is enabled), or no connection when that set is empty (ec2api/api/vpn_connection.py:356, ec2api/api/vpn_connection.py:447).

On Kube-OVN, subnet host routes and gateway_ip = null may not be honoured like in ML2/OVN; see the backend reference's Kube-OVN gaps.

Algorithm 6 — S3 bundle import

ec2api/api/image.py:846 to ec2api/api/image.py:1100: ImageLocation s3://bucket/key or bucket/key → manifest XML: machine_configuration/architecture (default x86_64), block_device_mapping/mapping{virtual,device}, kernel_id/ramdisk_id (EC2 IDs resolved to Glance UUIDs; the literal true marks the bundle itself as a kernel or ramdisk → aki/ari disk and container format), image/filename parts, image/ec2_encrypted_key and ec2_encrypted_iv (hex, RSA PKCS#1 v1.5 with x509_root_private_key, giving the hex AES-128 key and IV). Parts are concatenated, decrypted (AES-128-CBC), gunzipped and untarred (every member must stay inside the directory); the first tar member is uploaded to Glance. The Glance image is created first with image_state=pending, visibility=private and the manifest properties. OpenKeel: S3 comes from Swift s3api; the import should run as a job, not in the API process.

Algorithm 7 — Describe reconciliation, filters, paging

ec2api/api/common.py:389 (01 § 1.6, 05 § 2). Pseudo-code:

items   = ids ? project items with these ids (all must exist) : project items of KIND
os_objs = list OpenStack objects (kind-specific, user or admin session)
for o in os_objs:
    item = items_by_os_id[o.id]
    if selective and not (o.name in names or item.id in ids): continue
    if not item and KIND not in VPC_KINDS: item = add_item(KIND, {os_id: o.id} + extension)
    f = format(item, o); add tagSet
    drop o.name / item.id from the pending names/ids
    if f and not filtered_out(f): out += f
for item in items not paired: delete it (kind-specific: instances are detached and removed later)
if pending ids or names: raise <KIND>.NotFound(first pending)
return page(out)            # sort by SORT_KEY only when MaxResults/NextToken given

filtered_out: every filter must match; a filter matches when any of its values fnmatches any value found at the filter's path (dotted path through lists and sub-dicts, ec2api/api/common.py:344); a filter whose path yields no value filters the item out. tag filters match key equality and value wildcard. Filter names per describer: ec2-actions_17.0.0.json → describers. Paging: MaxResults ∈ [5, 1000], NextToken = base64(str(offset)).

Algorithm 8 — Instance metadata tree

ec2api/metadata/api.py:94 to ec2api/metadata/api.py:310. Versions 1.0 … 2009-04-04 and latest (= 2009-04-04). /<v>/ lists meta-data and user-data (if any). A version keeps the attributes introduced up to it (VERSION_DATA, ec2api/metadata/api.py:44); 2009-04-04 (and latest) keeps everything, including attributes listed nowhere (ancestor-ami-ids is listed for 2007-10-10…). Directory listing: sorted keys, sub-trees with /, the public key entry as 0=<key name>; leaves as text; lists joined by \n; text/plain. public-keys/0/openssh-key comes from the key pair of the instance's user (Nova admin lookup with user_id), user-data from OS-EXT-SRV-ATTR:user_data. cloud-init's EC2 datasource reads /<version>/meta-data/ recursively and /<version>/user-data, choosing among the versions listed at / [inferred from cloud-init's EC2 datasource; not run here]. OpenKeel: the backend reference provides metadata through Nova's metadata service or KubeVirt cloud-init (NoCloud/config drive); if ec2-api-metadata is not reimplemented, instances lose only the EC2-specific identifiers (i-…, ami-…, r-…) in their metadata — say so to users.

Algorithm 9 — Signature verification contract with Keystone

The Go service must send Keystone exactly what ec2-api sends (01 § 1.2, 03 § 2.1) so that the Keystone Go ec2tokens (which must reproduce keystoneclient:keystoneclient/contrib/ec2/utils.py:78) verifies the same requests:

  • params = all request parameters except Signature for SigV0/1/2; {} for SigV4 (header or query form).
  • host = the Host header with port; verb; path = request path; headers = all headers (original case); body_hash = hex SHA-256 of the raw body.
  • SigV4 query-string (presigned) requests cannot work through this contract (empty params, and the X-Amz-* parameters then reach the action). If they must be supported, send the query parameters for SigV4 GETs and strip X-Amz-* before dispatch.
  • Verification test vectors: the SigV2/SigV4 vectors of keystoneclient's own tests (keystoneclient:keystoneclient/tests/unit/test_ec2utils.py) and Keystone's keystone:keystone/tests/unit/test_contrib_ec2_core.py [inferred: not captured in this pack].

4. Client expectations

4.1 boto3 / AWS CLI (botocore) and aws-sdk-go (Terraform)

Checked against botocore 1.29.118's EC2 model (_rewrite/botocore-compat_17.0.0.md):

  • All 97 actions exist in the SDK model (API version 2016-11-15); 494 SDK operations are not implemented (InvalidRequest).
  • Response parsing works: 726 recorded success responses parse with botocore's EC2 parser and the operation's output shape; the 10 failures come from test fakes with an empty launchTime (an empty element for a timestamp). Real responses can contain empty elements too, because None is rendered as an empty element (ec2api/api/ec2utils.py:150): a Go rewrite must omit absent timestamps rather than emit <launchTime/>.
  • Error parsing works: <Response><Errors><Error><Code> is read as Error.Code/Message (checked in the report).
  • Elements botocore ignores: return on actions whose AWS response is a structure (StartInstances, StopInstances, CreateSecurityGroup, AssociateAddress, AssignPrivateIpAddresses); dnsNameV6, requesterManaged on instance ENIs, productCodesSet. **StartInstances/StopInstances return <return>true</return>** instead of instancesSet state changes: boto3 returns an empty StartingInstances/StoppingInstances; the AWS CLI prints nothing; Terraform's waiters then poll DescribeInstances [inferred].
  • Parameters the SDKs can send that ec2-api rejects with 500 (complete per-action list in the report). The ones clients send by default or in common commands:
    • DryRun — only when the user asks (--dry-run), never by default;
    • TagSpecification.N — sent by aws ec2 run-instances --tag-specifications, by **Terraform for every taggable resource created with tags** (aws_instance, aws_vpc, aws_subnet, aws_security_group, aws_eip, aws_ebs_volume…) [inferred from the provider's tag-on-create behaviour; not run here];
    • CidrIp, IpProtocol, FromPort, ToPort, SourceSecurityGroupName on Authorize/RevokeSecurityGroupIngress — sent by aws ec2 authorize-security-group-ingress --protocol … --port … --cidr …;
    • RunInstances: MetadataOptions, CreditSpecification, HibernationOptions, CapacityReservationSpecification, PrivateDnsNameOptions, MaintenanceOptions, LaunchTemplate… — sent by Terraform's aws_instance for several of them [inferred].
  • Actions Terraform needs that do not exist [inferred from the provider's resource read paths; verify with a real run]: DescribeVpcAttribute (aws_vpc), DescribeInstanceTypes and DescribeInstanceCreditSpecifications (aws_instance), DescribeNetworkAcls, DescribeSecurityGroupRules; the provider's start-up account checks call STS (not served by ec2-api; set skip_requesting_account_id, skip_credentials_validation, skip_region_validation, skip_metadata_api_check and an endpoints { ec2 = … } override).
  • SigV4 is signed for service ec2 and any region; POST form bodies are what botocore and aws-sdk-go send, which is the only SigV4 form that verifies (§ 3 algorithm 9).

4.2 euca2ools / boto2 (SigV2)

  • SigV2 parameters (AWSAccessKeyId, SignatureMethod, SignatureVersion=2, Timestamp) are verified by Keystone with params (ec2api/api/__init__.py:186); Timestamp within ±300 s (ec2api/api/__init__.py:51); Expires accepted (and then passed to the action → 500 [inferred: expires is not stripped by ec2api/api/__init__.py:252]).
  • boto2 sends InstanceId as a list for GetConsoleOutput/GetPasswordData (ec2api/api/instance.py:58); keep the one-element list form.
  • FaultWrapper errors are EC2 XML (500) for SigV2 clients (ec2api/api/faults.py:64).

4.3 Instance metadata consumers

Paths and formats of § 3 algorithm 8; X-Forwarded-For-based identity is set by the Neutron metadata proxy; 404 for anything unknown; text/plain.

5. Database strategy

  • Reuse the schema and the data (items, tags, 03 § 1): IDs must survive (§ 3 algorithm 1). Run the migrated schema (_rewrite/schema-sqlite-migrated_17.0.0.sql; MySQL options utf8/InnoDB). Keep a case-insensitive collation (utf8mb4_general_ci or the existing utf8_general_ci) if any query still relies on LIKE or ID case-insensitivity; prefer exact lower-case comparisons in Go and lower-case IDs on input.
  • migrate_version (sqlalchemy-migrate, version 1) is the only bookkeeping; a Go migration tool should treat version 1 as its baseline and never alter items/tags incompatibly while Python and Go may run side by side.
  • Integrity is kept by code: no foreign keys between items, tags or across kinds; tags are deleted with their item (ec2api/db/sqlalchemy/api.py:171).
  • Transactions: Python commits each statement (autocommit facade, ec2api/db/sqlalchemy/api.py:47) and compensates on errors (OnCrashCleaner). In Go, use a transaction per action for the DB part, but the OpenStack side still needs compensations in reverse order, as in Python; keep the order of creations (01 § 2) so that partial failures leave the same objects.
  • Concurrency: add optimistic checks (or SELECT … FOR UPDATE) on read-modify-write items (route tables, VPN connections, ENIs, addresses) — Python has none. Default VPC creation needs a cluster-wide lock (DB advisory lock or Kubernetes lease) instead of a file lock.
  • MariaDB Galera: random 32-bit IDs are fine for multi-writer clusters; the unique os_id index serialises concurrent auto-creation (keep the "duplicate → re-read and reuse" path of ec2api/db/sqlalchemy/api.py:107).
  • Secrets at rest: vpn.pre_shared_key is stored in clear JSON; keep the format but restrict database access.

6. Authentication, policies, configuration and CLI

  • No oslo.policy: ec2-api has no policy file and no role checks. Authorisation is (a) Keystone's signature check, (b) project scoping of every DB query, (c) whatever Nova/Neutron/Cinder/Glance enforce on the user's token, (d) the service user's admin rights for the admin-session reads (03 § 2.2). A rewrite should keep that model; OpenStack errors leak as EC2 errors with the OpenStack class name (02 § 1) — map them to AWS codes (UnauthorizedOperation for 403, <Kind>.NotFound for 404, *LimitExceeded for quota) deliberately.
  • Configuration (_rewrite/config_17.0.0.conf): options the API actually reads — keystone_ec2_tokens_url, ec2_timestamp_expiry, use_forwarded_for, external_network, disable_ec2_classic, default_flavor, ec2_private_dns_show_ip, network_device_mtu, nova_service_type, cinder_service_type, internal_service_availability_zone, my_ip, ec2_host, ec2_port, ec2_scheme, ec2_path, region_list, s3_url, s3_region, x509_root_private_key, image_decryption_dir, listeners and workers (ec2api_*, metadata_*, s3_listen*, buckets_path), [metadata] group, [keystone_authtoken] (service credentials and TLS for ec2tokens), [database], [cache], [oslo_concurrency] lock_path. cert_topic is dead (ec2api/api/image.py:66). fatal_exception_format_errors changes error messages (02 § 3). ec2_timestamp_expiry and disable_ec2_classic change client-visible behaviour.
  • CLI: ec2-api-manage db_sync [version], db_version (ec2api/cmd/manage.py:32). Processes: ec2-api, ec2-api-metadata, ec2-api-s3 (setup.cfg console_scripts). For OpenKeel: implement ec2-api and the manage commands; ec2-api-s3 is replaced by Swift s3api; ec2-api-metadata is optional (§ 3 algorithm 8).
  • EC2 credentials are created in Keystone (openstack ec2 credentials create, /v3/users/{id}/credentials/OS-EC2); Keystone must have ec2credential in [auth] methods (keystone:keystone/api/_shared/EC2_S3_Resource.py:71).

7. Conformance strategy

  • No black-box suite in the repository. The upstream black-box suite was ec2api-tempest-plugin (botocore against a devstack), not available here; this pack uses the unit tests instead.
  • Recorded unit-test exchanges (exchanges-api-tests_17.0.0.json): each exchange has the form request the test would send after authentication, the parsed XML response and the back-end calls. The OpenStack responses were mock fakes from ec2api/tests/unit/fakes.py, so the exchanges are evidence, not replayable cases: to use them as golden tests, a Go harness must fake OpenStack with the same fake objects (ec2api/tests/unit/fakes.py) and the DB items the test set (set_mock_items, ec2api/tests/unit/base.py:137). They are best used to check: XML element names and nesting per action (also in response-schemas-api-tests_17.0.0.md), error codes and messages (observed-errors-api-tests_17.0.0.md), and the OpenStack calls each action makes (backend_calls).
  • Capture command (adapter generate-reverse-spec/scripts/adapters/ec2_api.py, which rewrites ApiTestCase._execute to put the request on the wire): conformance_capture.py http ec2-api --branch 17.0.0 --adapter adapters/ec2_api.py --wrap ec2api.api:Executor --test-module <30 unit test modules>; EC2_CC_MODE=metadata … --wrap ec2api.metadata:MetadataRequestHandler --test-module ec2api.tests.unit.test_metadata; EC2_CC_MODE=s3 … --wrap ec2api.s3.s3server:S3Application --test-module ec2api.tests.unit.test_s3. All 396 + 22 + 4 + 9 tests pass under recording; test_hacking was not run (the hacking package is not installed).
  • Dispatcher probes (ec2-actions_17.0.0.json → probes): 15 requests through Requestify + Executor that pin the edge behaviour (unknown action/parameter, SigV4 query parameters, missing Action/Version, timestamps, malformed IDs, case of the action, value coercion).
  • Client check: tools/botocore_check.py reruns the botocore parse of any set of exchanges; point it at exchanges recorded from the Go service to compare.
  • End-to-end (to do against OpenKeel): run the AWS CLI and a minimal Terraform configuration (VPC, subnet, security group, key pair, instance, EIP, volume) against both, with the provider's skip flags (§ 4.1).

8. Gaps, non-functional requirements and checklist

Gaps found while building the pack

  • No functional tests in the repository; Terraform and AWS CLI behaviour in § 4.1 is partly [inferred].
  • The unit tests mock most helpers of the action under test, so backend_calls are incomplete for some actions (4 actions are not exercised through the API at all: DeleteSnapshot, ModifyInstanceAttribute, ResetImageAttribute, ResetInstanceAttribute; _rewrite/action-translation_17.0.0.md).
  • Keystone signature vectors are not captured (§ 3 algorithm 9).
  • No MySQL run of the migration (SQLite only); the MySQL-specific statements of ec2api/db/sqlalchemy/migrate_repo/versions/001_juno.py:48 are documented, not executed.

Non-functional requirements

  • One Keystone round trip per request (no cache): Keystone capacity sizes ec2-api's; a Go rewrite may cache successful verifications briefly (keyed by access key + signature + body hash) only if Keystone's expiry semantics are kept.
  • Describe operations list whole projects (05 § 2): budget Nova/Neutron/Cinder listing latency per call; the metadata cache (15 s) protects them for instances.
  • ModifyInstanceAttribute instanceType blocks a request for up to ~60 s (ec2api/api/instance.py:689); CreateImage stops a server for up to 1 h in the background.
  • Eventlet single process per worker; green threads are lost on restart (05 § 1).

Checklist

  • [ ] Action table, wire names and validators from ec2-actions_17.0.0.json; numeric list ordering; typed values.
  • [ ] Error XML and status rules (02 § 1); AWS-compatible codes; no Python class names leaking (or a compatibility flag).
  • [ ] Unknown parameters ignored (or flag); DryRun; TagSpecification support or a clear error.
  • [ ] Keystone ec2tokens contract (§ 3 algorithm 9); SigV2 and SigV4 POST; timestamp rules.
  • [ ] ID mapping, derived IDs, reservation IDs, naming of OpenStack objects, JSON format (§ 3 algorithm 1).
  • [ ] Describe reconciliation (auto-create, delete obsolete) and filters (§ 3 algorithm 7).
  • [ ] Block device mapping merge, instance states, route-table host routes, VPN connections (algorithms 3–5).
  • [ ] Admin session for the reads that need it; external network selection.
  • [ ] Default VPC with a cluster-wide lock (disable_ec2_classic).
  • [ ] Background jobs for CreateImage and S3 import, observable through imageState.
  • [ ] No empty elements for absent timestamps; StartInstances/StopInstances state changes if AWS-correct output is wanted.
  • [ ] Metadata service decision (reimplement or rely on Nova/KubeVirt) and S3 (Swift s3api).

Source: reverse-spec-ec2-api/07-rewrite-pack_17.0.0.md. The _rewrite/ and _inventory/ files are not published on the site.