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

Reverse-spec · Services

ec2-api — Query api

EC2 Query API, metadata API and S3 API — 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

ec2-api exposes three HTTP services. None of them has a REST route table:

ServiceProcessDefault listenerProtocol
EC2 Query APIec2-api0.0.0.0:8788 (ec2api/service.py:30, ec2api/service.py:33)AWS EC2 Query: form-encoded Action=…&Version=…, XML responses, AWS signature v2 or v4
Instance metadataec2-api-metadata0.0.0.0:8789 (ec2api/service.py:42, ec2api/service.py:45)EC2 instance metadata tree (/latest/meta-data/…), behind Neutron's metadata proxy
S3 object storeec2-api-s30.0.0.0:3334 (ec2api/s3/s3server.py:55, ec2api/s3/s3server.py:58)minimal S3 (buckets as directories), no authentication

The machine-readable catalogue of the 97 actions, with every parameter, is in [_rewrite/actions_17.0.0.md](_rewrite/actions_17.0.0.md) (generated from _rewrite/ec2-actions_17.0.0.json). The OpenStack calls each action made in the unit tests are in [_rewrite/action-translation_17.0.0.md](_rewrite/action-translation_17.0.0.md). This file gives the wire protocol, the dispatcher, and the functional description of each family of actions. Errors are in 02, back-end calls in 03.

1. EC2 Query API — wire protocol

1.1 Request pipeline

etc/ec2api/api-paste.ini:5 maps / to the ec2apicloud pipeline: ec2apifaultwrap logrequest ec2apikeystoneauth cloudrequest ec2apiexecutor (etc/ec2api/api-paste.ini:10), built by ec2api/api/auth.py:37. The process serves it with eventlet, URLs up to 16384 bytes (ec2api/cmd/api.py:34).

OrderFilterCodeWhat it does
1FaultWrapperec2api/api/__init__.py:62catches any exception raised below it and turns it into faults.Fault(HTTPInternalServerError) (ec2api/api/__init__.py:70). Fault answers 400 with webob's plain-text page when the request has no AWSAccessKeyId parameter (ec2api/api/faults.py:64), i.e. for every SigV4 client; SigV2 clients get the EC2 XML error with status 500
2RequestLoggingec2api/api/__init__.py:75one INFO access line per request: duration, remote address, method, path, action, status, user agent, content types (ec2api/api/__init__.py:96)
3EC2KeystoneAuthec2api/api/__init__.py:111extracts the signature and access key, has Keystone verify them (POST keystone_ec2_tokens_url), builds the request context (§ 1.2)
4Requestifyec2api/api/__init__.py:248timestamp check, drops the authentication parameters, builds the APIRequest (§ 1.3)
5Executorec2api/api/__init__.py:362dispatches the action, renders the XML response or the EC2 error (§ 1.4, § 1.5)

Authentication therefore happens before the timestamp check and before the action is even known: an unknown action costs a Keystone round trip.

1.2 Authentication (EC2KeystoneAuth)

  1. A request ID req-<uuid4> is generated (ec2api/api/__init__.py:164) and the SHA-256 hex digest of the raw body is computed first, because reading the parameters later consumes the body (ec2api/api/__init__.py:169).
  2. Signature: the Signature parameter (SigV0–2), else the X-Amz-Signature parameter (SigV4 query string), else the Signature= field of an Authorization: AWS4-HMAC-SHA256 … header (ec2api/api/__init__.py:124). Missing → 400 AuthFailure "Signature not provided" (ec2api/api/__init__.py:172).
  3. Access key: the AWSAccessKeyId parameter, else the first /-separated field of X-Amz-Credential, else of the Credential= field of the header (ec2api/api/__init__.py:144). Missing → 400 AuthFailure "Access key not provided" (ec2api/api/__init__.py:178).
  4. Signed parameters: for SigV4 (X-Amz-Signature parameter or any Authorization header) params is sent empty; otherwise every request parameter except Signature is sent (ec2api/api/__init__.py:182).
  5. The credentials {access, signature, host, verb, path, params, headers, body_hash} (ec2api/api/__init__.py:190) are POSTed as JSON to keystone_ec2_tokens_url (default http://localhost:5000/v3/ec2tokens, ec2api/api/__init__.py:48). The envelope is {"ec2Credentials": …} when the URL contains ec2, {"auth": {"OS-KSEC2:ec2Credentials": …}} otherwise (ec2api/api/__init__.py:203). host is the Host header with its port, headers all request headers. TLS verification follows [keystone_authtoken] cafile/insecure (ec2api/clients.py:159). There is no timeout on this call (ec2api/api/__init__.py:211) and no cache: every EC2 request is one Keystone authentication.
  6. Any non-200 answer becomes AuthFailure with Keystone's HTTP status and reason phrase as message (for example 401 AuthFailure "Unauthorized") (ec2api/api/__init__.py:213). A 200 whose body is not a token becomes 400 AuthFailure "Failure communicating with keystone" (ec2api/api/__init__.py:221).
  7. The token is wrapped in a keystoneauth session (ec2api/api/__init__.py:226); all later calls to Nova, Neutron, Glance and Cinder run as the user, with the user's token and the catalogue returned by Keystone. The context carries user, project, names, remote address (X-Forwarded-For when use_forwarded_for, ec2api/api/__init__.py:231) and api_version = the Version parameter (ec2api/api/__init__.py:235).

What Keystone checks (stable/2026.2, keystone:keystone/api/ec2tokens.py:39): it recomputes the signature with Ec2Signer (keystoneclient:keystoneclient/contrib/ec2/utils.py:78) — SigV0/1/2 according to SignatureVersion, otherwise SigV4 when the Authorization header or X-Amz-Algorithm says so. A SigV2 signature is retried without the port of host (keystone:keystone/api/ec2tokens.py:52). For SigV4 the canonical query string is empty for POST and computed from params otherwise (keystoneclient:keystoneclient/contrib/ec2/utils.py:240); since ec2-api sends empty params for SigV4, only POST (form body) SigV4 requests can verify [inferred: a SigV4 GET with query parameters signs a non-empty canonical query string]. The credential-scope date must prefix X-Amz-Date (keystoneclient:keystoneclient/contrib/ec2/utils.py:257); the region and service of the scope are taken from the request, so any region name is accepted. Keystone rejects requests older than [credential] auth_ttl minutes from Timestamp or X-Amz-Date (keystone:keystone/api/_shared/EC2_S3_Resource.py:46) and requires ec2credential in [auth] methods (keystone:keystone/api/_shared/EC2_S3_Resource.py:71).

1.3 Parameter decoding (Requestify, APIRequest)

  1. Timestamp (ec2api/api/__init__.py:255, ec2api/api/ec2utils.py:168): Timestamp and Expires together → 400 plain-text "Request must include either Timestamp or Expires, but cannot contain both". Expires in the past, or Timestamp more than ec2_timestamp_expiry (300 s, ec2api/api/__init__.py:51) away from now in either direction, or an unparsable value → **403 plain-text "Timestamp failed validation."** (ec2api/api/__init__.py:262). Formats %Y-%m-%dT%H:%M:%SZ or with 3–6 fractional digits (ec2api/api/ec2utils.py:165). SigV4 requests carry X-Amz-Date instead, which ec2-api does not check (Keystone does).
  2. Action missing → 400 plain text (ec2api/api/__init__.py:274). Version missing → KeyError outside the try (ec2api/api/__init__.py:286) → FaultWrapper → 400 plain text for SigV4 clients, 500 XML for SigV2 clients (probe in _rewrite/ec2-actions_17.0.0.json).
  3. The parameters Action, Signature, AWSAccessKeyId, SignatureMethod, SignatureVersion, Version, Timestamp are removed (ec2api/api/__init__.py:252); SignatureMethod is kept for SignatureVersion 1 (ec2api/api/__init__.py:268, bug lp:720157). **Expires and the SigV4 query parameters (X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-SignedHeaders, X-Amz-Signature, X-Amz-Security-Token) are not removed** and reach the action as unexpected arguments (§ 1.4).
  4. Dotted names → nested structures (ec2api/api/ec2utils.py:93): each .-separated segment is converted CamelCase → snake_case (ec2api/api/ec2utils.py:48); BlockDeviceMapping.1.Ebs.VolumeSize=10 becomes {'block_device_mapping': {'1': {'ebs': {'volume_size': 10}}}}. Then dicts whose first key is numeric become lists sorted as strings (ec2api/api/apirequest.py:70: Id.10 sorts before Id.2), and dicts with a single value key collapse to the value (ec2api/api/apirequest.py:74, for Attribute.Value= forms such as SourceDestCheck.Value).
  5. Value conversion (ec2api/api/ec2utils.py:52): every value string is converted: ''→'', 'None'→None, 'true'/'false' (any case) → booleans, 0x… hex, 0b… binary, **a leading 0 → octal** ('0755'→493, '08' stays the string), decimal integers, then floats ('1e5'→100000.0). This applies to every parameter, including names, descriptions and tag values: Tag.1.Value=123 becomes the integer 123 and CreateTags fails with 500 TypeError; GroupDescription=True fails with 500 UnboundLocalError; KeyName.1=0755 fails with a misleading ValidationError … should not be greater than 255 characters (probes in _rewrite/ec2-actions_17.0.0.json).
  6. Dispatch (ec2api/api/apirequest.py:49): getattr(VpcCloudController(), camelcase_to_underscore(Action)). An unknown name → 400 InvalidRequest "The request received was invalid." (ec2api/api/apirequest.py:56). The match is case-insensitive in practice: describeKeyPairs works, and the response root is describeKeyPairsResponse (the action string as sent, ec2api/api/apirequest.py:86).
  7. Per-action validation (ec2api/api/cloud.py:51): each controller stub declares one validator type per leading parameter. For each declared parameter present, the type's method of <module>.Validator (ec2api/api/common.py:95, extended in ec2api/api/instance.py:56, ec2api/api/dhcp_options.py:45, ec2api/api/route_table.py:38) checks it; a required parameter (no default) that is missing raises MissingParameter naming the Python parameter (The required parameter 'cidr_block' is missing, ec2api/api/cloud.py:75). Parameters after the declared types are not validated at all. The validator object receives the types of the parameters, not their values (ec2api/api/cloud.py:69), so security_group_str sees params.get('vpc_id') = the type name 'vpc_id' whenever the action declares a VpcId parameter: CreateSecurityGroup always applies the VPC character set to group names, even for EC2-Classic groups (ec2api/api/common.py:268, ec2api/api/validator.py:200). Validators: ec2api/api/validator.py:27 (str, max 255, message always says 255), :36 bool, :43 int, :51 list, :99 IPv4 CIDR, :122 VPC CIDR /16–/28, :127 subnet CIDR /16–/28, :136 EC2 ID (<prefix>-<hex>; any wrong prefix or non-hex suffix → InvalidParameterValue … Expected: <prefix>-...), :160 IPv4, :180 filters, :188 key/value lists, :197 security group names, :225 VPN type ipsec.1.
  8. The implementation is then called with all decoded parameters (ec2api/api/cloud.py:76). A parameter it does not declare raises TypeError → **500 TypeError "Unknown error occurred."**. This is what happens with DryRun (never supported, api-ref/source/supported_features.inc:7), with SigV4 query-string authentication, and with any parameter newer SDKs may send. _rewrite/botocore-compat_17.0.0.md lists, per action, the parameters botocore can serialise that ec2-api rejects this way.

1.4 Success response

ec2api/api/apirequest.py:83, ec2api/api/ec2utils.py:125:

  • Content-Type: text/xml, status 200, body <{Action}Response xmlns="http://ec2.amazonaws.com/doc/{Version}/"><requestId>req-…</requestId>…</{Action}Response> without XML declaration, pretty-printed by lxml (ec2api/api/apirequest.py:87, ec2api/api/__init__.py:395). The namespace echoes whatever Version the client sent; no version is checked or negotiated.
  • A handler returning True renders <return>true</return> (ec2api/api/apirequest.py:85).
  • Rendering: dict → child elements in insertion order; list → repeated <item>; bool → true/false; datetime → YYYY-MM-DDTHH:MM:SS.mmmZ; bytes → UTF-8 text; None → an empty element (not omitted) (ec2api/api/ec2utils.py:135). Timestamps coming from OpenStack as strings (Nova created, Cinder created_at, Glance created_at) are passed through unchanged.
  • The private key of CreateKeyPair is not logged (ec2api/api/apirequest.py:96).

1.5 Error response

ec2api/api/faults.py:28: Content-Type: text/xml, body

<?xml version="1.0"?>
<Response><Errors><Error><Code>{code}</Code><Message>{message}</Message></Error></Errors><RequestID>{request id}</RequestID></Response>

with XML escaping of the three values (ec2api/utils.py:51). Note RequestID (capital ID) here and requestId in success responses, as AWS does. The code, status and message come from ec2api/api/__init__.py:304; the full rules and the catalogue are in 02. Errors raised before the Executor (missing Action/Version, timestamp) are webob plain-text pages, not this XML.

1.6 Describe operations

All Describe* actions share UniversalDescriber.describe (ec2api/api/common.py:389), which matters for any reimplementation because describing also writes to the database:

  1. read the project's items of the kind (or the requested IDs; one missing ID → <Kind>.NotFound, ec2api/api/ec2utils.py:267), then list the OpenStack objects (get_os_items);
  2. pair them by os_id; an OpenStack object without an item gets one created on the fly for non-VPC kinds (instances, volumes, snapshots, images, security groups: ec2api/api/common.py:300); this is how resources created outside the EC2 API acquire EC2 IDs;
  3. an item without its OpenStack object is deleted as obsolete (ec2api/api/common.py:315, ec2api/api/common.py:431), with its tags;
  4. filters: Filter.N.Name must be in the describer's FILTER_MAP, else InvalidParameterValue … invalid filter (ec2api/api/common.py:327). Values are matched with shell wildcards (fnmatch on str(value), ec2api/api/common.py:320); several values of one filter are ORed, several filters ANDed (ec2api/api/common.py:323). tag:<key> is rewritten to a tag filter (ec2api/api/common.py:485). Booleans compare as True/False strings [inferred from str(value)]. The filter names per describer are in _rewrite/ec2-actions_17.0.0.json (describers);
  5. paging (only DescribeInstances, DescribeVolumes, DescribeSnapshots, DescribeTags): MaxResults < 5 → InvalidParameterValue (ec2api/api/common.py:391), capped at 1000; results sorted by the kind's ID; NextToken is base64 of the decimal offset (ec2api/api/common.py:360). MaxResults with explicit IDs → InvalidParameterCombination (ec2api/api/instance.py:429, ec2api/api/volume.py:178, ec2api/api/snapshot.py:102).

Tags are attached to taggable kinds as tagSet only when non-empty (ec2api/api/common.py:476).

2. Action families — functional description

Parameters of every action: [_rewrite/actions_17.0.0.md](_rewrite/actions_17.0.0.md). "Neutron" calls are made with the user's token unless stated otherwise; [admin] marks calls made with the service credentials of [keystone_authtoken] (ec2api/context.py:137, ec2api/clients.py:145).

2.1 Instances — RunInstances, TerminateInstances, DescribeInstances, Reboot/Stop/StartInstances, Describe/Modify/ResetInstanceAttribute, GetConsoleOutput, GetPasswordData

  • Purpose: launch, list and control Nova servers under EC2 instance IDs (i-xxxxxxxx) grouped in reservations (r-xxxxxxxx).
  • RunInstances (ec2api/api/instance.py:77):
    1. RamdiskId/KernelId → InvalidAttribute (ec2api/api/instance.py:88); counts must satisfy 1 ≤ min ≤ max (ec2api/api/instance.py:866).
    2. ClientToken: if an instance with this token exists, its reservation is returned (idempotency through a client-token describe filter, ec2api/api/instance.py:96).
    3. The image must be mapped and available (ec2api/api/instance.py:879; Glance status active or image_state). The flavor is the Nova flavor named InstanceType, default default_flavor = m1.small (ec2api/api/instance.py:44, ec2api/api/instance.py:1113); unknown → InvalidParameterValue on InstanceType.
    4. Block device mappings are merged with the image's (§ 07, algorithm 3; ec2api/api/instance.py:944). UserData is base64-decoded and passed to Nova (ec2api/api/instance.py:116). Placement.AvailabilityZone is passed through.
    5. Network: either EC2-Classic (security group names, one non-external tenant network whose name does not start with subnet-, ec2api/api/instance.py:1439) or VPC (subnet, private IP, security group IDs, or NetworkInterface.N), with the combination rules of ec2api/api/instance.py:1234 and ec2api/api/instance.py:1318. With disable_ec2_classic, a request without subnet goes to the first subnet of the default VPC (ec2api/api/instance.py:1279).
    6. A reservation ID r- + 8 characters of 0-9a-z is generated (ec2api/api/instance.py:1592). Then one Nova server per instance, sequentially (ec2api/api/instance.py:139): create the Neutron ports (VPC), nova.servers.create(name='<r-id>-<index>', image, flavor, min_count=1, max_count=1, availability_zone, block_device_mapping_v2, key_name, userdata, security_groups, nics) (ec2api/api/instance.py:147), add the i item {reservation_id, launch_index, client_token?, disable_api_termination?} with vpc_id (ec2api/api/instance.py:166), rename the server to its EC2 ID (ec2api/api/instance.py:170), and record the ENI attachment (ec2api/api/instance.py:1197).
    7. Failure on instance k: if k < MinCount, everything created is rolled back (servers deleted, items removed, ports deleted); once MinCount instances exist, later failures keep the created ones (cleaner.approveChanges(), ec2api/api/instance.py:141) [inferred: the exception still propagates, so the client gets an error although instances were kept].
    8. The response is the reservation as DescribeInstances formats it (ec2api/api/instance.py:175), instances typically pending (Nova building).
  • TerminateInstances (ec2api/api/instance.py:186): refuses the whole call if one instance has disable_api_termination (OperationNotPermitted); otherwise server.delete() for each, and returns previous/current states (re-reading the server; gone → terminated). Items are not deleted: the next describe removes them when Nova no longer lists the server (ec2api/api/instance.py:208), detaching ENIs and deleting those with delete_on_termination (ec2api/api/instance.py:844).
  • Reboot/Stop/Start (ec2api/api/instance.py:445): all instances must exist in Nova (_remove_instances + NotFound otherwise) and be in an allowed vm_state (reboot: active, stopped, paused, suspended, error; stop: active, rescued, error; start: stopped), else IncorrectInstanceState; then reboot() (soft), stop(), start(). Force is ignored. The response is <return>true</return>, not the AWS state-change list (StopInstances/StartInstances in _rewrite/botocore-compat_17.0.0.md).
  • DescribeInstances (ec2api/api/instance.py:427): servers are listed with the admin session: servers.list(all_tenants=True, project_id=<project>), or servers.get for a single ID (ec2api/api/instance.py:339), plus Cinder volumes and Nova flavors with the user session. Formatting (ec2api/api/instance.py:730): state from OS-EXT-STS:vm_state (§ 07 algorithm 4); instanceType = flavor name or unknown; imageId mapped from image.id (creating an ami mapping if needed); placement.availabilityZone, keyName, launchTime = Nova created, rootDeviceName, block devices from Cinder attachments (ec2api/api/instance.py:1529), kernel/ramdisk as aki/ari. VPC instances get networkInterfaceSet, vpcId, subnetId, groupSet, sourceDestCheck, private/public IP from the primary ENI; Classic instances get IPs from Nova addresses and groups from Nova security_groups (ec2api/api/instance.py:766). privateDnsName is OS-EXT-SRV-ATTR:hostname or the IP (ec2_private_dns_show_ip, ec2api/api/instance.py:809). Admin contexts see keyName (project, host) (ec2api/api/instance.py:817, never set by the API path [inferred]). Reservations group instances by reservation_id with ownerId = server tenant_id (ec2api/api/instance.py:302).
  • Attributes (ec2api/api/instance.py:492, ec2api/api/instance.py:578, ec2api/api/instance.py:709): describe supports blockDeviceMapping, disableApiTermination, groupSet, sourceDestCheck (VPC only), instanceType, kernel, ramdisk, rootDeviceName, userData (admin field OS-EXT-SRV-ATTR:user_data); others → InvalidParameterValue "Unknown attribute.". Modify supports exactly one of disableApiTermination (DB only), groupSet (VPC: Neutron port security groups of the single ENI), sourceDestCheck (VPC: port allowed_address_pairs), instanceType (only when stopped: Nova resize, polling up to 45 s for resized, then confirm_resize and up to 15 s more, ec2api/api/instance.py:670). Reset supports sourceDestCheck only.
  • GetConsoleOutput / GetPasswordData (ec2api/api/instance.py:479, ec2api/api/instance.py:464): Nova get_console_output / get_password, base64-encoded, timestamp = now. A list of one ID is accepted (boto sends one) (ec2api/api/instance.py:58).
  • Lifecycle:
stateDiagram-v2
  [*] --> pending: RunInstances (Nova building)
  pending --> running: Nova vm_state active
  running --> stopped: StopInstances (Nova stop)
  stopped --> running: StartInstances
  running --> running: RebootInstances (soft)
  running --> shutting_down: TerminateInstances (Nova deleted / soft-delete)
  stopped --> shutting_down: TerminateInstances
  shutting_down --> terminated: server gone from Nova (wiped_out)
  terminated --> [*]: next Describe* removes the item

2.2 Images — CreateImage, RegisterImage, DeregisterImage, DescribeImages, Describe/Modify/ResetImageAttribute

  • Purpose: expose Glance images as ami-/aki-/ari- (kind from Glance container_format: aki/kernel → aki, ari/ramdisk → ari, otherwise ami, ec2api/api/image.py:84, ec2api/api/image.py:811).
  • CreateImage (ec2api/api/image.py:109): the instance must boot from a Cinder volume (InvalidParameterValue "Instance does not have a volume attached at root (null)."). Without NoReboot and with the server running, an ami item without os_id is created at once and an eventlet green thread stops the server, polls it every second (up to 1 h) until SHUTOFF, calls Nova createImage(name, metadata={'ec2_id': <ami-id>}), stores the image UUID, and restarts the server; failure sets state=failed on the item (ec2api/api/image.py:135). Otherwise the snapshot is synchronous. Default name image of <server uuid> at <time>. BlockDeviceMapping is ignored.
  • RegisterImage (ec2api/api/image.py:198): ImageLocation with no scheme or s3:// → S3 bundle import (§ 07 algorithm 6, asynchronous); another URL scheme → Glance image container_format=bare, disk_format=raw, visibility=private then Glance web-download import of the URL (ec2api/api/image.py:285); no location but RootDeviceName → an empty Glance image (upload of 0 bytes) carrying root_device_name, bdm_v2=True and the JSON block_device_mapping built from BlockDeviceMapping (volume sizes read from Cinder snapshots/volumes) (ec2api/api/image.py:239). Architecture, KernelId, RamdiskId become image properties.
  • DeregisterImage (ec2api/api/image.py:310): only the owner project; deletes the Glance image (404 ignored) and the item. An image still being created → IncorrectState.
  • DescribeImages (ec2api/api/image.py:485): the project's images plus every public image (items found by get_public_items), Glance images.list() with the user token. ExecutableBy and Owner are accepted and ignored. Public images of other projects get ID-only mappings with no project (ec2api/api/image.py:424), so every project sees the same ami- ID. Pending images (CreateImage in progress) are shown from the item with imageState=pending or failed (ec2api/api/image.py:453). State mapping ec2api/api/image.py:94, S3 import states ec2api/api/image.py:835.
  • Attributes (ec2api/api/image.py:492, ec2api/api/image.py:554, ec2api/api/image.py:642): describe blockDeviceMapping, description (from the item), launchPermission (group=all when public), kernel, ramdisk, rootDeviceName; others → InvalidRequest. Modify launchPermission add/remove for group all only → Glance visibility=public/private; description → item. Reset launchPermission → private. Owner check → AuthFailure "Not authorized for image:<uuid>" (ec2api/api/image.py:653).

2.3 Volumes and snapshots — CreateVolume, AttachVolume, DetachVolume, DeleteVolume, DescribeVolumes, CreateSnapshot, DeleteSnapshot, DescribeSnapshots

  • CreateVolume (ec2api/api/volume.py:38): ClientToken idempotency; Cinder volumes.create(size, snapshot_id, volume_type, availability_zone); item vol; the Cinder volume's display_name is set to the EC2 ID. Iops, Encrypted, KmsKeyId are accepted and ignored.
  • AttachVolume (ec2api/api/volume.py:75): Nova volumes.create_server_volume(server, volume, device); Nova 409/400 → UnsupportedOperation. Returns the attachment from Cinder with deleteOnTermination=false.
  • DetachVolume (ec2api/api/volume.py:96): finds the server from the Cinder attachment (the InstanceId, Device, Force parameters are ignored); not attached → IncorrectState; Nova delete_server_volume.
  • DeleteVolume (ec2api/api/volume.py:117): Cinder delete; 400 → UnsupportedOperation; 404 ignored. The item is removed by a later describe.
  • DescribeVolumes (ec2api/api/volume.py:176): Cinder volumes.list() (user), Nova servers [admin] for deleteOnTermination. Status reserved/attaching/detaching → in-use (ec2api/api/volume.py:195); attachmentSet from the first Cinder attachment.
  • CreateSnapshot (ec2api/api/snapshot.py:32): volume status must be available, in-use, attaching or detaching; Cinder volume_snapshots.create(volume, force=True); item snap; snapshot display_name = EC2 ID, display_description = Description.
  • DescribeSnapshots (ec2api/api/snapshot.py:99): Cinder snapshots of the project; status map ec2api/api/snapshot.py:122 (deleted snapshots are hidden). Owner and RestorableBy are ignored.

2.4 Key pairs — CreateKeyPair, ImportKeyPair, DeleteKeyPair, DescribeKeyPairs

No database: key pairs are Nova key pairs of the user, identified by name (ec2api/api/key_pair.py:41). CreateKeyPair generates a 2048-bit RSA key in ec2-api (ec2api/api/key_pair.py:89), stores only the OpenSSH public key in Nova and returns the PEM private key (TraditionalOpenSSL) in keyMaterial. ImportKeyPair expects base64 PublicKeyMaterial (ec2api/api/key_pair.py:128). Nova 409 → InvalidKeyPair.Duplicate, over limit → ResourceLimitExceeded; DeleteKeyPair ignores 404 (ec2api/api/key_pair.py:138).

2.5 Tags — CreateTags, DeleteTags, DescribeTags

Tags live only in ec2-api's tags table (ec2api/api/tag.py:49). Keys 1–127 characters, not starting with aws:; values ≤ 255; resource kinds ec2api/api/tag.py:29 (others → InvalidID). Resources must exist in the project's items, except images (any ami/aki/ari ID is accepted, ec2api/api/tag.py:74). An existing key is overwritten (ec2api/db/sqlalchemy/api.py:265). DeleteTags without Tag removes every tag of the resources; a tag pair with only Key matches any value (ec2api/db/sqlalchemy/api.py:275); it never fails on unknown IDs. DescribeTags filters: key, tag-key, resource-id, resource-type, value, tag-value (ec2api/api/tag.py:95), paged and sorted by key.

2.6 Availability zones, regions, account attributes

  • DescribeAvailabilityZones (ec2api/api/availability_zone.py:97): Nova availability_zones.list(detailed=False) minus internal_service_availability_zone; ZoneName.1=verbose returns the host/service tree (ec2api/api/availability_zone.py:175).
  • DescribeRegions (ec2api/api/availability_zone.py:108): built from configuration only (region_list of name=host, else one RegionOne with ec2_scheme://ec2_host:ec2_port ec2_path). RegionName is ignored.
  • DescribeAccountAttributes (ec2api/api/availability_zone.py:133): supported-platforms (EC2,VPC or VPC only), default-vpc (ID or none), max-instances (Nova quota); others → InvalidParameter.

2.7 VPC — CreateVpc, DeleteVpc, DescribeVpcs

  • CreateVpc (ec2api/api/vpc.py:50, ec2api/api/vpc.py:127): CIDR /16–/28 IPv4. Creates a Neutron router (over quota → VpcLimitExceeded), the vpc item {cidr_block, is_default, route_table_id} with os_id = router, the main route table item with the local route, renames the router to the VPC ID, and creates the default security group: a Neutron security group named after the VPC ID whose EC2 ID is sg-<same 8 hex as the VPC> (ec2api/api/security_group.py:114). InstanceTenancy is ignored.
  • Default VPC (ec2api/api/vpc.py:169): with disable_ec2_classic=True and a non-admin context, the first describe of VPCs, subnets, route tables, security groups or internet gateways (and several other paths) creates a VPC 172.31.0.0/16 with is_default, an internet gateway attached to it, a subnet 172.31.0.0/20 and a 0.0.0.0/0 route to the gateway, under the file lock ec2api-default-vpc-lock-<project> (ec2api/api/vpc.py:173). Failures are logged and swallowed (ec2api/api/vpc.py:184).
  • DeleteVpc (ec2api/api/vpc.py:55): refused (DependencyViolation) while the VPC has subnets, an internet gateway, a VPN gateway, more than the main route table or more than the default security group; then deletes the main route table item, the default security group and the router (409 logged, 404 ignored).
  • DescribeVpcs (ec2api/api/vpc.py:120): items only (no Neutron call); state always available, dhcpOptionsId defaults to default (ec2api/api/vpc.py:194).

2.8 Subnets — CreateSubnet, DeleteSubnet, DescribeSubnets

  • CreateSubnet (ec2api/api/subnet.py:42): CIDR /16–/28 inside the VPC (InvalidSubnet.Range). Creates a Neutron network per subnet and a Neutron IPv4 subnet with the host routes computed from the main route table (§ 07 algorithm 5), plugs it into the VPC router (400 → InvalidSubnet.Conflict), adds the subnet item, starts VPN services if a VPN gateway is attached, renames network and subnet to the EC2 ID and sets the subnet gateway IP (ec2api/api/subnet.py:84). AvailabilityZone is ignored.
  • DeleteSubnet (ec2api/api/subnet.py:98): refused while ENIs exist; removes the item, VPN connections, the router interface and the Neutron network (in use → logged).
  • DescribeSubnets (ec2api/api/subnet.py:175): availableIpAddressCount = 2^(32−prefix) − 2 − ports with an IP in the subnet − 1 if no DHCP/distributed service port was counted (ec2api/api/subnet.py:182); defaultForAz and mapPublicIpOnLaunch always false.

2.9 Route tables — CreateRouteTable, DeleteRouteTable, DescribeRouteTables, CreateRoute, ReplaceRoute, DeleteRoute, AssociateRouteTable, ReplaceRouteTableAssociation, DisassociateRouteTable, Enable/DisableVgwRoutePropagation

Route tables exist only in ec2-api's database (rtb item {vpc_id, routes[], propagating_gateways[]}, ec2api/api/route_table.py:304); they are realised as **Neutron subnet host_routes and gateway_ip** on every subnet associated with the table (explicitly, or implicitly through the VPC's main table) and as VPN peer_cidrs (§ 07 algorithm 5).

  • Routes target exactly one of GatewayId (igw-/vgw- of the same VPC), NetworkInterfaceId, InstanceId (its single ENI) (ec2api/api/route_table.py:329); VpcPeeringConnectionId → InvalidRequest. A route inside the VPC CIDR is refused; an identical existing route makes CreateRoute idempotent, a different one → RouteAlreadyExists; the local route cannot be deleted.
  • Association IDs are derived, not stored: rtbassoc-<hex of the subnet> for explicit associations and rtbassoc-<hex of the VPC> for the main association (ec2api/api/route_table.py:165, ec2api/api/route_table.py:520). ReplaceRouteTableAssociation on the main association changes the VPC's main table.
  • Route state: active when the gateway is attached to the VPC or the ENI's instance is ACTIVE in Nova, else blackhole (ec2api/api/route_table.py:444); origin CreateRouteTable, CreateRoute or EnableVgwRoutePropagation. DescribeRouteTables calls Nova once per instance route.
  • Every change rewrites the Neutron subnets concerned and is rolled back on failure (ec2api/api/route_table.py:569).

2.10 Internet gateways — Create/Attach/Detach/Delete/DescribeInternetGateways

DB-only igw items (ec2api/api/internet_gateway.py:40). Attach: the gateway must be free and the VPC without another one; unless a VPN gateway already did it, sets the external network as the router's gateway (add_gateway_router with the network of get_os_public_network, ec2api/api/internet_gateway.py:59). The external network is the single router:external network, or the one named external_network (ec2api/api/ec2utils.py:457); none or several → Unsupported "Feature is restricted by OS admin". Detach removes the router gateway unless a VPN gateway is attached.

2.11 Elastic IPs — AllocateAddress, AssociateAddress, DisassociateAddress, ReleaseAddress, DescribeAddresses

Neutron floating IPs on the external network (ec2api/api/address.py:209).

  • Domain=vpc (or any domain when disable_ec2_classic) creates an eipalloc item {public_ip}; standard (EC2-Classic) floating IPs have no item and no allocation ID (ec2api/api/address.py:223).
  • VPC associate (ec2api/api/address.py:287): target ENI (or the single ENI of the instance), default private IP = ENI primary IP; the VPC must have an internet gateway (Gateway.NotAttached); already associated elsewhere without AllowReassociation → Resource.AlreadyAssociated; Neutron update_floatingip(port_id, fixed_ip_address); association ID eipassoc-<hex of the allocation>. Classic associate: Nova add_floating_ip(server, ip).
  • Release: VPC addresses need the allocation ID; an associated address → InvalidIPAddress.InUse (with disable_ec2_classic, addresses of the default VPC are disassociated first) (ec2api/api/address.py:233).
  • Describe reconciles: an item whose floating IP lost its port or fixed IP is disassociated in the DB (ec2api/api/address.py:124).

2.12 Network interfaces — Create/Delete/Describe/Attach/DetachNetworkInterface, Describe/Modify/ResetNetworkInterfaceAttribute, Assign/UnassignPrivateIpAddresses

Neutron ports (ec2api/api/network_interface.py:45). Create: IPs validated against the subnet CIDR, one primary, SecondaryPrivateIpAddressCount adds {'subnet_id'} fixed IPs (default one IP); security groups default to the VPC's default group and must belong to the VPC; Neutron errors map to InsufficientFreeAddressesInSubnet / InvalidParameterValue; the port is renamed to the eni- ID and gets the VPC's DHCP options (§ 2.13). The eni item holds {subnet_id, description, private_ip_address, source_dest_check?, instance_id?, device_index?, attach_time?, delete_on_termination?}. Attach: Nova interface_attach(server, port); attachment ID eni-attach-<hex of the ENI>; detach (index 0 refused) **only clears the port's device_id/device_owner** in Neutron (ec2api/api/network_interface.py:493) [inferred: Nova is not asked to detach]. sourceDestCheck=false = port allowed_address_pairs [0.0.0.0/0] (ec2api/api/network_interface.py:403). requesterManaged = port owner starts with network:.

2.13 DHCP options — Create/Delete/Describe/AssociateDhcpOptions

DB-only dopt items {dhcp_configuration: {key: [values]}} (ec2api/api/dhcp_options.py:60); keys domain-name-servers, domain-name, ntp-servers, netbios-name-servers, netbios-node-type. Association (ec2api/api/dhcp_options.py:127, default = none) sets the VPC's dhcp_options_id and pushes Neutron extra_dhcp_opts (mtu = network_device_mtu plus the mapped options, ec2api/api/dhcp_options.py:160) to every ENI of the project, not only those of the VPC (ec2api/api/dhcp_options.py:138, ec2api/api/dhcp_options.py:147).

2.14 Security groups — Create/Delete/DescribeSecurityGroups, Authorize/RevokeSecurityGroupIngress/Egress

Neutron security groups (ec2api/api/security_group.py:57). default is reserved; duplicates (per VPC, or among Classic groups) → InvalidGroup.Duplicate; with disable_ec2_classic groups go to the default VPC. Rules come only from IpPermissions.N (IpProtocol tcp/udp/icmp or a number, ports, first Groups or first IpRanges entry, ec2api/api/security_group.py:288); the EC2-Classic top-level form (CidrIp, IpProtocol, FromPort, ToPort, SourceSecurityGroupName) is not declared and fails with 500 (§ 1.3 step 8; the AWS CLI --protocol/--port/--cidr shortcut uses it). Egress needs a VPC group. Revoke deletes the Neutron rules whose significant fields match (ec2api/api/security_group.py:361); a missing rule → InvalidPermission.NotFound for VPC groups, silently ignored for Classic groups. Describe repairs missing default groups of VPCs (ec2api/api/security_group.py:182), shows a VPC's default group as default (ec2api/api/security_group.py:404), and expands a Classic "any protocol" ingress rule into icmp/tcp/udp (ec2api/api/security_group.py:494). Deleting the default group → CannotDelete; Neutron 409 → DependencyViolation.

2.15 VPN — Create/Attach/Detach/Delete/DescribeVpnGateways, Create/Delete/DescribeCustomerGateways, Create/Delete/DescribeVpnConnections, Create/DeleteVpnConnectionRoute

Neutron VPNaaS (ec2api/api/vpn_gateway.py:37, ec2api/api/vpn_connection.py:46, ec2api/api/customer_gateway.py:32). vgw and cgw are DB-only items; attaching a VPN gateway sets the router's external gateway if needed and **creates one Neutron vpnservice per subnet** of the VPC (ec2api/api/vpn_gateway.py:173). A VPN connection (static routes only: Options.StaticRoutesOnly=true is required, else Unsupported) creates an IKE policy (v1, sha1, aes-128, group2, main, 28800 s) and an IPsec policy (esp, sha1, aes-128, group2, tunnel, 3600 s), a random 32-character pre-shared key, and then **one ipsec_site_connection per subnet** whose route table sends traffic to the VPN gateway (ec2api/api/vpn_connection.py:356, § 07 algorithm 5). One customer gateway can have only one connection (InvalidCustomerGateway.DuplicateIpAddress). customerGatewayConfiguration is an XML document generated from the Neutron objects (ec2api/api/vpn_connection.py:260). Customer gateways: BgpAsn must be 65000 or absent.

3. Instance metadata API (ec2-api-metadata)

etc/ec2api/api-paste.ini:31: ec2apifaultwrap logrequest metaapp → MetadataRequestHandler (ec2api/metadata/__init__.py:79). It sits behind the Neutron metadata proxy (or an NSX proxy) and is reached at http://169.254.169.254/ by instances (the route to 169.254.169.254/32 is pushed by ec2-api itself into subnet host routes, ec2api/api/route_table.py:643).

PathResult
/ or /ec2/version list 1.0 … 2009-04-04 and latest, one per line (ec2api/metadata/api.py:32, ec2api/metadata/api.py:69)
/<version>/meta-data/…, /<version>/user-dataEC2 metadata tree of the calling instance, cut down to what <version> defined (ec2api/metadata/api.py:266); unknown version → 404
/openstack/…proxied to Nova's metadata service (nova_metadata_ip:port, signed X-Instance-ID-Signature) (ec2api/metadata/__init__.py:122)
  • Requester identification (ec2api/metadata/__init__.py:188): Neutron proxy headers X-Instance-ID, X-Tenant-ID, X-Forwarded-For, X-Instance-ID-Signature = HMAC-SHA256(metadata_proxy_shared_secret, instance ID) (ec2api/metadata/__init__.py:245); or NSX X-Metadata-Provider + X-Forwarded-For (+ optional X-Metadata-Provider-Signature), the instance then being found from Neutron ports [admin] (ec2api/metadata/api.py:73). Missing headers → 400, bad signature → 403, no X-Forwarded-For → 500 (EC2MetadataInvalidAddress is not caught specifically, ec2api/metadata/__init__.py:115).
  • Content (ec2api/metadata/api.py:167): built by running DescribeInstances for the instance with the admin session scoped to the tenant (ec2api/metadata/__init__.py:266): ami-id, ami-launch-index, ami-manifest-path (FIXME), block-device-mapping/{ami,root,ebsN}, hostname, instance-id, instance-type, local-hostname, local-ipv4, placement/availability-zone, public-hostname, public-ipv4, reservation-id, security-groups, kernel-id, ramdisk-id, public-keys/0=<name>/openssh-key (fetched from Nova with the server's user_id, ec2api/metadata/api.py:221), and user-data (base64-decoded OS-EXT-SRV-ATTR:user_data). The project of the instance must match X-Tenant-ID (ec2api/metadata/api.py:155).
  • Cache: oslo.cache region, key ec2api-metadata-<instance uuid>, cache_expiration 15 s (ec2api/metadata/api.py:102).
  • Rendering (ec2api/metadata/api.py:276): dict → sorted keys, sub-dicts with a trailing / (or their _name), lists joined by newlines; text/plain.

4. S3 API (ec2-api-s3)

A port of nova-objectstore (ec2api/s3/s3server.py:75), used only as a source of AMI bundles for RegisterImage (s3_url, ec2api/api/image.py:52). Buckets are directories under buckets_path. Routes (ec2api/s3/s3server.py:88): GET / list buckets; GET|PUT|DELETE|HEAD /<bucket>; GET|PUT|DELETE /<bucket>/<object>. No request is authenticated (no signature check anywhere in ec2api/s3/s3server.py); any client that reaches the port can create, read and delete objects. Errors: 404 NoSuchKey XML, 403 with an empty body (existing bucket on PUT, non-empty bucket on DELETE), 501 BadRequest "Unhandled error" on any exception (ec2api/s3/s3server.py:150). Path containment is checked with startswith(directory) without a trailing separator (ec2api/s3/s3server.py:256) [inferred: a sibling directory sharing the prefix, e.g. buckets2, is reachable as ../buckets2]. In OpenKeel, S3 is provided by Swift's s3api, not by this server.

Source: reverse-spec-ec2-api/01-query-api_17.0.0.md. The _rewrite/ and _inventory/ files are not published on the site.