Interactions — 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-29Other repositories cited:
keystone:= openstack/keystonestable/2026.2@e6dc5ae07(the OpenKeel target, not the Keystone ec2-api 17.0.0 was released with);keystoneclient:= python-keystoneclientstable/2026.2@b76de6a3;novaclient:/neutronclient:= local clones used only for exception attributes.
ec2-api uses no message bus (no oslo.messaging import anywhere in ec2api/), no Redis, and no notifications. It talks to its own database, to Keystone, Nova, Neutron, Cinder and Glance over REST, to an S3 endpoint (image bundles), to Nova's metadata service, to an oslo.cache back end (metadata), and runs openssl.
| System | Protocol / library | Direction | Credentials | Configuration | Used by |
|---|---|---|---|---|---|
| MariaDB/MySQL (or SQLite) | SQL, oslo.db EngineFacade (legacy, autocommit=True) | out | [database] connection | ec2api/db/sqlalchemy/api.py:40 | every action (ID mapping, DB-only resources, tags) |
Keystone ec2tokens | HTTPS requests.request('POST'), JSON | out | none (the request signature is the credential) | keystone_ec2_tokens_url (ec2api/api/__init__.py:48) | every EC2 request |
| Keystone catalogue / identity | keystoneauth session, keystoneclient v3 | out | user token; service user of [keystone_authtoken] | ec2api/clients.py:43 | endpoint discovery, ec2.list (S3 import) |
| Nova | novaclient, microversion up to 2.32 | out | user token; admin session for some reads | nova_service_type = compute (ec2api/clients.py:28) | instances, key pairs, AZs, quotas, volume attach, images (createImage) |
| Neutron | neutronclient v2.0 | out | user token; admin for NSX metadata lookups | service type network (ec2api/clients.py:76) | VPC, subnets, ports, routers, floating IPs, security groups, VPNaaS |
| Cinder | cinderclient, version from the endpoint URL | out | user token | cinder_service_type = volumev3 (ec2api/clients.py:34) | volumes, snapshots |
| Glance | glanceclient v2 | out | user token | service type image (ec2api/clients.py:81) | images |
| S3 endpoint | botocore S3 client, SigV4 | out | the user's first EC2 credential fetched from Keystone | s3_url, s3_region (ec2api/api/image.py:52) | RegisterImage of an S3 bundle |
| Nova metadata service | httplib2 | out | HMAC of the instance ID with metadata_proxy_shared_secret | [metadata] nova_metadata_* (ec2api/metadata/__init__.py:37) | /openstack/… metadata paths |
| Neutron/NSX metadata proxy | HTTP | in | same HMAC | [metadata] metadata_proxy_shared_secret | instance metadata |
| oslo.cache | dogpile back end (memcached…) | out | — | [cache] (ec2api/metadata/__init__.py:76) | metadata cache |
openssl binary | processutils.execute | local | — | — | AES-128-CBC decryption of S3 bundles (ec2api/api/image.py:1037) |
| Local files | — | local | — | buckets_path, image_decryption_dir, [oslo_concurrency] lock_path, x509_root_private_key | S3 server, image import, default-VPC lock |
1. Database
1.1 Schema
Migrations: one sqlalchemy-migrate script, ec2api/db/sqlalchemy/migrate_repo/versions/001_juno.py:20 (no Alembic). Models: ec2api/db/sqlalchemy/models.py:41. The migrated SQLite schema is _rewrite/schema-sqlite-migrated_17.0.0.sql, the model DDL _rewrite/schema-mysql_17.0.0.sql; they are identical apart from the migrate_version bookkeeping table and the MySQL table options (_rewrite/schema-diff_17.0.0.md).
| Table | Columns | Keys | Content |
|---|---|---|---|
items | id VARCHAR(30), project_id VARCHAR(64), vpc_id VARCHAR(12), os_id VARCHAR(36), data TEXT | PK id; **UNIQUE items_os_id_idx (os_id)** across all kinds and projects | one row per EC2 object: the EC2 ID, its owner project (NULL for ID-only mappings), the VPC it belongs to, the OpenStack UUID it maps to (NULL for DB-only objects), and a JSON blob of the other attributes |
tags | project_id VARCHAR(64), item_id VARCHAR(30), key VARCHAR(127), value VARCHAR(255) | PK (project_id, item_id, key); no FK | EC2 tags |
MySQL: ENGINE=InnoDB, CHARSET=utf8 (utf8mb3) on both tables, and the database default charset is altered to utf8 (ec2api/db/sqlalchemy/migrate_repo/versions/001_juno.py:48). With the default utf8_general_ci collation, id, os_id and the LIKE queries below are case-insensitive.
1.2 The item model
_pack_item_data / _unpack_item_data (ec2api/db/sqlalchemy/api.py:319): the Python dict of an item is stored as os_id and vpc_id columns plus data = json.dumps(rest) (Python's default separators ", " and ": "), id excluded. Reading merges them back. The kind is **the prefix of id**; queries by kind use id LIKE '<kind>-%'.
| Kind (prefix) | OpenStack object (os_id) | vpc_id column | data keys | Created by |
|---|---|---|---|---|
i | Nova server | VPC of the instance | reservation_id, launch_index, client_token?, disable_api_termination? | RunInstances (ec2api/api/instance.py:157); auto-create from Nova OS-EXT-SRV-ATTR:reservation_id/launch_index (ec2api/api/instance.py:1470) |
ami, aki, ari | Glance image (NULL while CreateImage runs) | — | is_public, description?, state? (failed) | CreateImage, RegisterImage, auto-create (ec2api/api/image.py:820) |
vol | Cinder volume | — | — | CreateVolume, auto-create |
snap | Cinder snapshot | — | — | CreateSnapshot, auto-create |
vpc | Neutron router | — | cidr_block, is_default, route_table_id, dhcp_options_id? | CreateVpc (ec2api/api/vpc.py:136) |
subnet | Neutron subnet | VPC | route_table_id? (explicit association), os_vpnservice_id? | CreateSubnet (ec2api/api/subnet.py:78) |
eni | Neutron port | VPC | subnet_id, description, private_ip_address, source_dest_check?, instance_id?, device_index?, attach_time?, delete_on_termination? | CreateNetworkInterface (ec2api/api/network_interface.py:148) |
sg | Neutron security group | VPC or NULL (Classic) | — | CreateSecurityGroup, default group, auto-create |
eipalloc | Neutron floating IP | — | public_ip, network_interface_id?, private_ip_address? | AllocateAddress (VPC) (ec2api/api/address.py:228) |
rtb | — | VPC | routes [{destination_cidr_block, gateway_id | network_interface_id}], propagating_gateways? | CreateVpc, CreateRouteTable (ec2api/api/route_table.py:304) |
igw | — | attached VPC or NULL | — | CreateInternetGateway |
dopt | — | — | dhcp_configuration {key: [values]} | CreateDhcpOptions |
vgw | — | attached VPC or NULL | — | CreateVpnGateway |
cgw | — | — | ip_address | CreateCustomerGateway |
vpn | — | — | customer_gateway_id, vpn_gateway_id, **pre_shared_key (clear text)**, os_ikepolicy_id, os_ipsecpolicy_id, cidrs, os_ipsec_site_connections {subnet ID: Neutron connection UUID} | CreateVpnConnection (ec2api/api/vpn_connection.py:89) |
Not stored: key pairs (Nova, by name), availability zones, reservations (r- IDs are a field of instances), and the derived IDs rtbassoc-, eipassoc-, eni-attach- (§ 07 algorithm 1).
1.3 Queries (reconstructed SQL)
All queries go through model_query without soft-delete filtering (ec2api/db/sqlalchemy/api.py:80). Each function runs in autocommit mode: no transaction spans an action; consistency relies on OnCrashCleaner compensations (ec2api/api/common.py:41).
| Function | SQL | Source |
|---|---|---|
add_item | INSERT INTO items (id, project_id, os_id, vpc_id, data) VALUES ('<kind>-<8 hex>', :project, :os_id, :vpc_id, :json); on duplicate os_id: SELECT * FROM items WHERE os_id = :os_id AND (project_id = :project OR project_id IS NULL) AND id LIKE '<kind>-%' (exactly one row expected), merge the new data into it, set project_id = :project, UPDATE | ec2api/db/sqlalchemy/api.py:98 |
add_item_id | INSERT INTO items (id, os_id[, project_id]) VALUES (…); on duplicate os_id: SELECT id FROM items WHERE os_id = :os_id (any project, any kind) | ec2api/db/sqlalchemy/api.py:126 |
update_item | SELECT * FROM items WHERE project_id = :project AND id = :id (one row, else NoResultFound); refuse to change a non-NULL os_id; UPDATE items SET os_id, vpc_id, data | ec2api/db/sqlalchemy/api.py:147 |
delete_item | DELETE FROM items WHERE project_id = :project AND id = :id; if a row went: DELETE FROM tags WHERE project_id = :project AND item_id = :id (errors ignored) | ec2api/db/sqlalchemy/api.py:162 |
restore_item | INSERT with the given id (rollback of a delete); duplicate → EC2DBDuplicateEntry | ec2api/db/sqlalchemy/api.py:182 |
get_items | SELECT * FROM items WHERE project_id = :project AND id LIKE '<kind>-%' | ec2api/db/sqlalchemy/api.py:197 |
get_item_by_id | SELECT * FROM items WHERE project_id = :project AND id = :id LIMIT 1 | ec2api/db/sqlalchemy/api.py:206 |
get_items_by_ids | SELECT * FROM items WHERE project_id = :project AND id IN (…) | ec2api/db/sqlalchemy/api.py:214 |
get_public_items | SELECT * FROM items WHERE id LIKE '<kind>-%' AND data LIKE '%"is_public": True%' [AND id IN (…)] — all projects; matches the JSON "is_public": true only because LIKE is case-insensitive (MySQL _ci collations, SQLite ASCII), and only with Python's ": " separator | ec2api/db/sqlalchemy/api.py:225 |
get_items_ids | SELECT id, os_id FROM items WHERE id LIKE '<kind>-%' [AND id IN (…)] [AND os_id IN (…)] — all projects | ec2api/db/sqlalchemy/api.py:236 |
add_tags | one transaction; per tag a savepoint INSERT INTO tags; on duplicate UPDATE tags SET value = :v WHERE project_id = :p AND item_id = :i AND key = :k | ec2api/db/sqlalchemy/api.py:248 |
delete_tags | DELETE FROM tags WHERE project_id = :p AND item_id IN (…) [AND ((key = :k [AND value = :v]) OR …)] | ec2api/db/sqlalchemy/api.py:275 |
get_tags | SELECT item_id, key, value FROM tags WHERE project_id = :p [AND (item_id LIKE 'k1-%' OR …)] [AND item_id IN (…)] | ec2api/db/sqlalchemy/api.py:302 |
Every function requires a user or admin context (ec2api/db/sqlalchemy/api.py:67, ec2api/context.py:128). The admin context has project_id = None (ec2api/context.py:140), so project-filtered queries return nothing for it; the metadata service sets the instance's project on its admin context first (ec2api/metadata/__init__.py:272).
Describe operations are the heaviest users: DescribeInstances reads instances, ENIs (with their own describe: addresses, ports, security groups), volumes, images and public images, tags (ec2api/api/instance.py:325); see _rewrite/action-translation_17.0.0.md for observed counts.
1.4 Locking and concurrency
- The only lock is the oslo.concurrency external (file) lock around default-VPC creation,
ec2api-default-vpc-lock-<project>-(ec2api/api/vpc.py:37,ec2api/api/vpc.py:175). It serialises processes on one host sharinglock_path; replicas on several hosts can each create a default VPC. - No
SELECT … FOR UPDATE, no optimistic versioning: two concurrent modifications of the same item (a route table, a VPN connection'scidrs) are last-writer-wins [inferred]. - Item IDs are random (
random.randint(1, 0xffffffff), Mersenne Twister,ec2api/db/sqlalchemy/api.py:91); a collision is not retried.
2. Keystone
2.1 POST /v3/ec2tokens (every request)
Frame sent by ec2-api for a SigV4 request (shape from ec2api/api/__init__.py:190; values illustrative):
POST /v3/ec2tokens HTTP/1.1
Content-Type: application/json
{"ec2Credentials": {
"access": "a1b2c3…",
"signature": "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7",
"host": "ec2.example.org:8788",
"verb": "POST",
"path": "/",
"params": {},
"headers": {"Host": "ec2.example.org:8788", "X-Amz-Date": "20230622T101500Z",
"Authorization": "AWS4-HMAC-SHA256 Credential=a1b2c3…/20230622/RegionOne/ec2/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d67…",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8", "Content-Length": "43", "User-Agent": "Boto3/…"},
"body_hash": "<sha256 hex of 'Action=DescribeInstances&Version=2016-11-15'>"}}
For SigV2, params holds every request parameter except Signature (AWSAccessKeyId, SignatureMethod, SignatureVersion=2, Timestamp, Action, Version, …). Keystone answers 200 with X-Subject-Token and a v3 token body (keystone:keystone/api/ec2tokens.py:76); ec2-api reads it with keystoneclient.access.AccessInfo.factory (ec2api/api/__init__.py:219). Errors: 401 (bad or missing signature, expired), 404 (unknown access key, keystone:keystone/api/_shared/EC2_S3_Resource.py:71), 503 when ec2credential is not enabled. No timeout, no retry, no caching.
2.2 Service catalogue and service credentials
- All OpenStack calls use the session built from the user's token (
ec2api/api/__init__.py:229): endpoints come from the catalogue in the token (public interface by default [inferred: keystoneauth default]). - The admin session (
[keystone_authtoken]auth plugin and session options,ec2api/clients.py:44,ec2api/clients.py:145) is a process-wide singleton used for: Novaservers.list(all_tenants=True, project_id=…)andservers.getin DescribeInstances, DescribeVolumes, DescribeInstanceAttribute,_is_ebs_instance, instance auto-creation (ec2api/api/instance.py:342,ec2api/api/instance.py:494,ec2api/api/instance.py:1126,ec2api/api/instance.py:1472,ec2api/api/volume.py:165); everything in the metadata service (ec2api/metadata/__init__.py:267). The service user therefore needs Nova's admin rules forall_tenantslisting and the extended attributes (OS-EXT-SRV-ATTR:*). keystone.ec2.list(user_id)(keystoneclient v3/v3/users/{id}/credentials/OS-EC2) with the user's token, to sign S3 calls with the user's first EC2 credential (ec2api/api/image.py:1072).
3. Nova
Version negotiation (ec2api/clients.py:102), once per process: GET the compute endpoint's version document; if the current version is v2.1, use microversion 2.32 when supported, else the maximum supported; a non-2.1 endpoint uses legacy 2. 2.3 brings the OS-EXT-SRV-ATTR fields, 2.10 the user_id keypair lookup, 2.32 BDM tags (ec2api/clients.py:52).
| Call | REST | Actions |
|---|---|---|
servers.create(name, image, flavor, min_count=1, max_count=1, availability_zone, block_device_mapping_v2, key_name, userdata, security_groups, nics) | POST /servers | RunInstances (ec2api/api/instance.py:147) |
servers.update(server, name=<i-id>) | PUT /servers/{id} | RunInstances (ec2api/api/instance.py:170) |
servers.delete / server.delete() | DELETE /servers/{id} | RunInstances rollback, TerminateInstances (ec2api/api/instance.py:204) |
servers.get | GET /servers/{id} | most instance actions, route states (ec2api/api/route_table.py:493), metadata |
servers.list(search_opts={'all_tenants': True, 'project_id': p}) [admin] | GET /servers/detail?all_tenants=True&project_id=p | DescribeInstances, DescribeVolumes |
server.reboot() / stop() / start() | POST /servers/{id}/action reboot (SOFT) / os-stop / os-start | Reboot/Stop/StartInstances (ec2api/api/instance.py:445) |
server.resize(flavor), confirm_resize() | POST …/action resize, confirmResize | ModifyInstanceAttribute instanceType (ec2api/api/instance.py:683) |
server.get_console_output() / get_password() | os-getConsoleOutput / GET /servers/{id}/os-server-password | GetConsoleOutput, GetPasswordData |
server.create_image(name, metadata) | POST …/action createImage | CreateImage (ec2api/api/image.py:154) |
flavors.list() / flavors.get(id) | GET /flavors/detail | RunInstances, DescribeInstances, attribute instanceType |
keypairs.list/create/delete, keypairs._get('/os-keypairs/<name>?user_id=<id>') | /os-keypairs | key pairs; metadata public key (ec2api/metadata/api.py:221) |
availability_zones.list(detailed) | GET /os-availability-zone[/detail] | DescribeAvailabilityZones |
quotas.get(project, user) | GET /os-quota-sets/{p}?user_id=u | DescribeAccountAttributes |
volumes.create_server_volume / delete_server_volume | POST/DELETE /servers/{id}/os-volume_attachments | AttachVolume, DetachVolume |
servers.interface_attach(server, port, None, None) | POST /servers/{id}/os-interface | AttachNetworkInterface (ec2api/api/network_interface.py:468) |
servers.add_floating_ip / remove_floating_ip | addFloatingIp / removeFloatingIp actions — removed from Nova in microversion 2.44 | EC2-Classic Associate/DisassociateAddress (ec2api/api/address.py:321, ec2api/api/address.py:410) [inferred: works only because 2.32 is pinned] |
4. Neutron
| Call | REST | Actions |
|---|---|---|
create_router({'router': {}}), update_router(name), delete_router | /v2.0/routers | Create/DeleteVpc |
add_gateway_router(router, {'network_id': ext}), remove_gateway_router | PUT /v2.0/routers/{id} external_gateway_info | Attach/DetachInternetGateway, Attach/DetachVpnGateway |
add_interface_router / remove_interface_router({'subnet_id'}) | PUT /v2.0/routers/{id}/add_router_interface | Create/DeleteSubnet |
create_network({'network': {'name': 'subnet-0'}}), update_network(name), delete_network | /v2.0/networks | Create/DeleteSubnet |
create_subnet({network_id, ip_version 4, cidr, host_routes}), update_subnet(name, gateway_ip, host_routes), show_subnet, list_subnets | /v2.0/subnets | subnets, every route-table change (ec2api/api/route_table.py:576) |
create_port({network_id, security_groups, fixed_ips}), update_port(name / fixed_ips / security_groups / allowed_address_pairs / extra_dhcp_opts / device_id='' device_owner=''), delete_port, show_port, list_ports(tenant_id) | /v2.0/ports | network interfaces, RunInstances (VPC), DHCP options, addresses |
list_networks(router:external=True, name=external_network) | /v2.0/networks | external network (ec2api/api/ec2utils.py:459) |
create_floatingip({floating_network_id}), update_floatingip(port_id, fixed_ip_address), delete_floatingip, show_floatingip, list_floatingips(tenant_id) | /v2.0/floatingips | elastic IPs |
create_security_group(name, description), delete_security_group, show_security_group, list_security_groups(tenant_id), create_security_group_rule, delete_security_group_rule | /v2.0/security-groups, /v2.0/security-group-rules | security groups |
create/update/delete_vpnservice, create/update/delete_ikepolicy, create/update/delete_ipsecpolicy, create/update/delete_ipsec_site_connection, list_ikepolicies/ipsecpolicies/ipsec_site_connections/routers(tenant_id) | /v2.0/vpn/* (VPNaaS) | VPN gateways and connections |
list_subnets(advanced_service_providers=[id]), list_ports(fixed_ips='ip_address=…', network_id) [admin] | /v2.0/subnets, /v2.0/ports | NSX metadata requester lookup (ec2api/metadata/api.py:73) |
tenant_id=<project> filters are passed on most list calls; Neutron error classes are mapped to EC2 codes at the call sites listed in 02 § 3.1.
5. Cinder and Glance
- Cinder (
ec2api/clients.py:86): the API version is deduced from the endpoint URL (get_volume_api_from_url). Calls:volumes.create(size, snapshot_id, volume_type, availability_zone),volume.update(display_name),volumes.get/list/delete,volume_snapshots.create(volume_id, force=True),snapshot.update(display_name, display_description),volume_snapshots.get/list/delete. - Glance v2 (
ec2api/clients.py:81):images.list(),images.get,images.create(**properties),images.upload(id, data, image_size),images.image_import(id, method='web-download', uri),images.update(id, visibility | image_state),images.delete. Image properties written by ec2-api:name,image_location,root_device_name,bdm_v2,block_device_mapping(JSON string),mappings(S3 manifests),architecture,kernel_id,ramdisk_id,image_state,ec2_id(Nova createImage metadata) (ec2api/api/image.py:221,ec2api/api/image.py:155).
6. S3 (image bundles)
_s3_conn (ec2api/api/image.py:1070): botocore S3 client on s3_url, region s3_region, signature s3v4, keys = access/secret of the first EC2 credential Keystone lists for the user. get_object(Bucket, Key) for the manifest and each part (ec2api/api/image.py:863, ec2api/api/image.py:1013). ClientErrors become EC2 errors (ec2api/api/__init__.py:377).
7. Metadata service exchanges
- Inbound from the Neutron metadata agent:
GET /latest/meta-data/…withX-Instance-ID,X-Tenant-ID,X-Forwarded-For,X-Instance-ID-Signature(ec2api/metadata/__init__.py:202). - Outbound to Nova metadata for
/openstack/…: the same path and query, headersX-Forwarded-For,X-Instance-ID,X-Tenant-ID,X-Instance-ID-Signature(recomputed), optional client certificate (ec2api/metadata/__init__.py:173). - Cache:
GET/SET ec2api-metadata-<instance uuid>holding{metadata, owner_id}, expirationcache_expiration(ec2api/metadata/api.py:102). Without[cache] enabled, a warning is logged and every request rebuilds the metadata (one DescribeInstances plus Nova calls) (ec2api/metadata/__init__.py:83).
8. Local processes and files
openssl enc -d -aes-128-cbc -in <file> -K <key> -iv <iv> -out <file>(ec2api/api/image.py:1037); key and IV decrypted with the RSA private keyx509_root_private_key(PKCS#1 v1.5,ec2api/api/image.py:1091). The key appears on the command line of the child process.- Temporary directories under
image_decryption_dir(default/tmp) for S3 imports (ec2api/api/image.py:891); bucket directories underbuckets_path(ec2api/s3/s3server.py:52).
Source: reverse-spec-ec2-api/03-interactions_17.0.0.md. The _rewrite/ and _inventory/ files are not published on the site.