Rewrite pack — ec2-api — branch 17.0.0
Branch
17.0.0@32740781f605— repository https://opendev.org/openstack/ec2-api (retired; local branchpre-retirement) — analysed on 2026-10-29Target:
openkeel-ec2-api(Go), seegenerate-go-from-spec/references/backends/ec2-api.md. Other repositories cited:keystone:= openstack/keystonestable/2026.2@e6dc5ae07;keystoneclient:= python-keystoneclientstable/2026.2@b76de6a3. Extraction environment: Python 3.10, requirements constrained byopenstack/requirementstag2023.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/
| File | Content | Use 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 probes | the 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 table | reading |
[action-translation_17.0.0.md](_rewrite/action-translation_17.0.0.md) | OpenStack and DB calls each action made in the unit tests | cross-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 exchanges | same, for the other services |
response-schemas-*-tests_17.0.0.{json,md} | response schemas inferred per action and status | XML element names and nesting per action |
observed-errors-*-tests_17.0.0.md | (action, status, code, message) seen in the tests | error 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 SQLite | the production schema |
schema-mysql_17.0.0.sql, schema-postgresql_17.0.0.sql | DDL generated from the models | MariaDB 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.py | the 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
- One endpoint:
POST /(andGET /?…) withActionandVersion; responsestext/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. - 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). - Version: never checked; echoed as the response namespace
http://ec2.amazonaws.com/doc/<Version>/(ec2api/api/apirequest.py:87). botocore sends2016-11-15. - 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 withUnknownParameter, but ignoring is what keeps SDK defaults working) and rejectDryRun=truewithUnsupportedOperationor answerDryRunOperation; keep the Python behaviour only behind a compatibility flag if bug-for-bug conformance is wanted. Decide explicitly; the probes inec2-actions_17.0.0.jsonshow the Python behaviour. - 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. - Request ID:
req-<uuid4>(ec2api/api/__init__.py:164), inrequestId(success) orRequestID(error). - No content negotiation, no
Content-Lengthlimits beyond eventlet's URL limit (16384,ec2api/cmd/api.py:34) andmax_header_line(config_17.0.0.conf). - 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):
| ID | Derived from | Source |
|---|---|---|
sg-<h> default security group of a VPC | vpc-<h>; the item is inserted with this exact ID (restore_item) | ec2api/api/security_group.py:106 |
rtbassoc-<h> explicit association | subnet-<h> | ec2api/api/route_table.py:165 |
rtbassoc-<h> main association | vpc-<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:
- 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). - 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. - Items are only visible to their project (
project_id = caller) except through the ID lookups above andget_public_items. - Cross-kind collisions:
_format_volume_mappingmaps a volume UUID with kindvoland puts it insnapshotId(ec2api/api/image.py:776);add_item_idon a duplicateos_idreturns the existing ID whatever its kind (ec2api/db/sqlalchemy/api.py:140). - 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 snapshotdisplay_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 connectionsvpn-…/subnet-…(ec2api/api/vpn_connection.py:414); the default security group of a VPC is the Neutron group **namedvpc-<h>** (ec2api/api/security_group.py:118), which_translate_group_namerelies on (ec2api/api/security_group.py:410); images made by CreateImage carry the Glance propertyec2_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. - The
dataJSON must stay readable by Python ec2-api during a migration: Pythonjson.dumpsdefault separators (", ",": "),true/false/null, keys as in 03 § 1.2. In particularget_public_itemsmatches the substring"is_public": Truecase-insensitively (ec2api/db/sqlalchemy/api.py:228): a Go writer that emits"is_public":truehides public images from Python readers. A Go reader should parse the JSON instead of usingLIKE.
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):
- Parse
BlockDeviceMapping.N:DeviceName,Ebs.SnapshotId(snap-→source_type=snapshot,vol-→source_type=volume(OpenStack extension), elseInvalidSnapshotID.Malformed),Ebs.VolumeSize,Ebs.DeleteOnTermination;destination_type=volume.VirtualNameandNoDeviceare ignored. The same device given twice with different/dev/prefixes →InvalidBlockDeviceMapping; identical → the last wins. - Image mappings: virtual
mappings(swap, ephemeralN) become blank local BDMs;block_device_mappingis v2 whenbdm_v2is set, else legacy and converted;delete_on_terminationdefaults to false; same device → replaced. - Root device = image
root_device_name, else therootentry ofmappings(ec2api/api/ec2utils.py:520). - For each requested BDM: not in the image → defaults
delete_on_termination=True,source_type=blank,boot_index0 if it is the root device else −1. In the image: the device name must match exactly (/dev/vdavsvda→ error); the root snapshot cannot be changed; a smallervolume_sizethan the image's → error ("Volume of size …"); a newsnapshot_idis allowed only on EBS image devices; otherwise the image BDM (withoutNonevalues) is updated with the request. <source_type>_idis moved touuid(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_state | EC2 name | code |
|---|---|---|
None, building | pending | 0 |
active | running | 16 |
deleted, soft-delete | shutting-down | 32 |
(gone from Nova) wiped_out | terminated | 48 |
stopped | stopped | 80 |
paused | pause | 80 |
suspended | suspend | 80 |
rescued | rescue | 16 |
resized | resize | 16 |
error, shelved, shelved_offloaded, anything else | the 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 exceptSignaturefor SigV0/1/2;{}for SigV4 (header or query form).host= theHostheader 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 theX-Amz-*parameters then reach the action). If they must be supported, send the query parameters for SigV4 GETs and stripX-Amz-*before dispatch. - Verification test vectors: the SigV2/SigV4 vectors of keystoneclient's own tests (
keystoneclient:keystoneclient/tests/unit/test_ec2utils.py) and Keystone'skeystone: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, becauseNoneis 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 asError.Code/Message(checked in the report). - Elements botocore ignores:
returnon actions whose AWS response is a structure (StartInstances,StopInstances,CreateSecurityGroup,AssociateAddress,AssignPrivateIpAddresses);dnsNameV6,requesterManagedon instance ENIs,productCodesSet. **StartInstances/StopInstances return<return>true</return>** instead ofinstancesSetstate changes: boto3 returns an emptyStartingInstances/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 byaws ec2 run-instances --tag-specifications, by **Terraform for every taggable resource created withtags** (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,SourceSecurityGroupNameon Authorize/RevokeSecurityGroupIngress — sent byaws ec2 authorize-security-group-ingress --protocol … --port … --cidr …;- RunInstances:
MetadataOptions,CreditSpecification,HibernationOptions,CapacityReservationSpecification,PrivateDnsNameOptions,MaintenanceOptions,LaunchTemplate… — sent by Terraform'saws_instancefor 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),DescribeInstanceTypesandDescribeInstanceCreditSpecifications(aws_instance),DescribeNetworkAcls,DescribeSecurityGroupRules; the provider's start-up account checks call STS (not served by ec2-api; setskip_requesting_account_id,skip_credentials_validation,skip_region_validation,skip_metadata_api_checkand anendpoints { ec2 = … }override). - SigV4 is signed for service
ec2and 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 withparams(ec2api/api/__init__.py:186);Timestampwithin ±300 s (ec2api/api/__init__.py:51);Expiresaccepted (and then passed to the action → 500 [inferred:expiresis not stripped byec2api/api/__init__.py:252]). - boto2 sends
InstanceIdas 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_cior the existingutf8_general_ci) if any query still relies onLIKEor 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 alteritems/tagsincompatibly 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_idindex serialises concurrent auto-creation (keep the "duplicate → re-read and reuse" path ofec2api/db/sqlalchemy/api.py:107). - Secrets at rest:
vpn.pre_shared_keyis 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 (
UnauthorizedOperationfor 403,<Kind>.NotFoundfor 404,*LimitExceededfor 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_topicis dead (ec2api/api/image.py:66).fatal_exception_format_errorschanges error messages (02 § 3).ec2_timestamp_expiryanddisable_ec2_classicchange 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.cfgconsole_scripts). For OpenKeel: implementec2-apiand the manage commands;ec2-api-s3is replaced by Swifts3api;ec2-api-metadatais optional (§ 3 algorithm 8). - EC2 credentials are created in Keystone (
openstack ec2 credentials create,/v3/users/{id}/credentials/OS-EC2); Keystone must haveec2credentialin[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 fromec2api/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 inresponse-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 rewritesApiTestCase._executeto 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_hackingwas not run (thehackingpackage 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, missingAction/Version, timestamps, malformed IDs, case of the action, value coercion). - Client check:
tools/botocore_check.pyreruns 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_callsare 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:48are 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 instanceTypeblocks 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;TagSpecificationsupport or a clear error. - [ ] Keystone
ec2tokenscontract (§ 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/StopInstancesstate 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.