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 branchpre-retirement) — analysed on 2026-10-29
ec2-api exposes three HTTP services. None of them has a REST route table:
| Service | Process | Default listener | Protocol |
|---|---|---|---|
| EC2 Query API | ec2-api | 0.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 metadata | ec2-api-metadata | 0.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 store | ec2-api-s3 | 0.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).
| Order | Filter | Code | What it does |
|---|---|---|---|
| 1 | FaultWrapper | ec2api/api/__init__.py:62 | catches 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 |
| 2 | RequestLogging | ec2api/api/__init__.py:75 | one INFO access line per request: duration, remote address, method, path, action, status, user agent, content types (ec2api/api/__init__.py:96) |
| 3 | EC2KeystoneAuth | ec2api/api/__init__.py:111 | extracts the signature and access key, has Keystone verify them (POST keystone_ec2_tokens_url), builds the request context (§ 1.2) |
| 4 | Requestify | ec2api/api/__init__.py:248 | timestamp check, drops the authentication parameters, builds the APIRequest (§ 1.3) |
| 5 | Executor | ec2api/api/__init__.py:362 | dispatches 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)
- 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). - Signature: the
Signatureparameter (SigV0–2), else theX-Amz-Signatureparameter (SigV4 query string), else theSignature=field of anAuthorization: AWS4-HMAC-SHA256 …header (ec2api/api/__init__.py:124). Missing →400 AuthFailure "Signature not provided"(ec2api/api/__init__.py:172). - Access key: the
AWSAccessKeyIdparameter, else the first/-separated field ofX-Amz-Credential, else of theCredential=field of the header (ec2api/api/__init__.py:144). Missing →400 AuthFailure "Access key not provided"(ec2api/api/__init__.py:178). - Signed parameters: for SigV4 (
X-Amz-Signatureparameter or anyAuthorizationheader)paramsis sent empty; otherwise every request parameter exceptSignatureis sent (ec2api/api/__init__.py:182). - The credentials
{access, signature, host, verb, path, params, headers, body_hash}(ec2api/api/__init__.py:190) are POSTed as JSON tokeystone_ec2_tokens_url(defaulthttp://localhost:5000/v3/ec2tokens,ec2api/api/__init__.py:48). The envelope is{"ec2Credentials": …}when the URL containsec2,{"auth": {"OS-KSEC2:ec2Credentials": …}}otherwise (ec2api/api/__init__.py:203).hostis theHostheader with its port,headersall 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. - Any non-200 answer becomes
AuthFailurewith Keystone's HTTP status and reason phrase as message (for example401 AuthFailure "Unauthorized") (ec2api/api/__init__.py:213). A 200 whose body is not a token becomes400 AuthFailure "Failure communicating with keystone"(ec2api/api/__init__.py:221). - 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-Forwhenuse_forwarded_for,ec2api/api/__init__.py:231) andapi_version= theVersionparameter (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)
- Timestamp (
ec2api/api/__init__.py:255,ec2api/api/ec2utils.py:168):TimestampandExpirestogether →400plain-text "Request must include either Timestamp or Expires, but cannot contain both".Expiresin the past, orTimestampmore thanec2_timestamp_expiry(300 s,ec2api/api/__init__.py:51) away from now in either direction, or an unparsable value → **403plain-text "Timestamp failed validation."** (ec2api/api/__init__.py:262). Formats%Y-%m-%dT%H:%M:%SZor with 3–6 fractional digits (ec2api/api/ec2utils.py:165). SigV4 requests carryX-Amz-Dateinstead, which ec2-api does not check (Keystone does). Actionmissing →400plain text (ec2api/api/__init__.py:274).Versionmissing →KeyErroroutside thetry(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).- The parameters
Action,Signature,AWSAccessKeyId,SignatureMethod,SignatureVersion,Version,Timestampare removed (ec2api/api/__init__.py:252);SignatureMethodis kept for SignatureVersion 1 (ec2api/api/__init__.py:268, bug lp:720157). **Expiresand 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). - 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=10becomes{'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.10sorts beforeId.2), and dicts with a singlevaluekey collapse to the value (ec2api/api/apirequest.py:74, forAttribute.Value=forms such asSourceDestCheck.Value). - Value conversion (
ec2api/api/ec2utils.py:52): every value string is converted:''→'','None'→None,'true'/'false'(any case) → booleans,0x…hex,0b…binary, **a leading0→ 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=123becomes the integer 123 andCreateTagsfails with 500 TypeError;GroupDescription=Truefails with 500 UnboundLocalError;KeyName.1=0755fails with a misleadingValidationError … should not be greater than 255 characters(probes in_rewrite/ec2-actions_17.0.0.json). - 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:describeKeyPairsworks, and the response root isdescribeKeyPairsResponse(the action string as sent,ec2api/api/apirequest.py:86). - 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 inec2api/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 raisesMissingParameternaming 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), sosecurity_group_strseesparams.get('vpc_id')= the type name'vpc_id'whenever the action declares aVpcIdparameter:CreateSecurityGroupalways 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),:36bool,:43int,:51list,:99IPv4 CIDR,:122VPC CIDR /16–/28,:127subnet CIDR /16–/28,:136EC2 ID (<prefix>-<hex>; any wrong prefix or non-hex suffix →InvalidParameterValue … Expected: <prefix>-...),:160IPv4,:180filters,:188key/value lists,:197security group names,:225VPN typeipsec.1. - The implementation is then called with all decoded parameters (
ec2api/api/cloud.py:76). A parameter it does not declare raisesTypeError→ **500TypeError"Unknown error occurred."**. This is what happens withDryRun(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.mdlists, 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 whateverVersionthe client sent; no version is checked or negotiated.- A handler returning
Truerenders<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 (Novacreated, Cindercreated_at, Glancecreated_at) are passed through unchanged. - The private key of
CreateKeyPairis 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:
- 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); - 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; - an item without its OpenStack object is deleted as obsolete (
ec2api/api/common.py:315,ec2api/api/common.py:431), with its tags; - filters:
Filter.N.Namemust be in the describer'sFILTER_MAP, elseInvalidParameterValue … invalid filter(ec2api/api/common.py:327). Values are matched with shell wildcards (fnmatchonstr(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 atagfilter (ec2api/api/common.py:485). Booleans compare asTrue/Falsestrings [inferred fromstr(value)]. The filter names per describer are in_rewrite/ec2-actions_17.0.0.json(describers); - paging (only
DescribeInstances,DescribeVolumes,DescribeSnapshots,DescribeTags):MaxResults< 5 →InvalidParameterValue(ec2api/api/common.py:391), capped at 1000; results sorted by the kind's ID;NextTokenis base64 of the decimal offset (ec2api/api/common.py:360).MaxResultswith 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):RamdiskId/KernelId→InvalidAttribute(ec2api/api/instance.py:88); counts must satisfy 1 ≤ min ≤ max (ec2api/api/instance.py:866).ClientToken: if an instance with this token exists, its reservation is returned (idempotency through aclient-tokendescribe filter,ec2api/api/instance.py:96).- The image must be mapped and
available(ec2api/api/instance.py:879; Glance statusactiveorimage_state). The flavor is the Nova flavor namedInstanceType, defaultdefault_flavor = m1.small(ec2api/api/instance.py:44,ec2api/api/instance.py:1113); unknown →InvalidParameterValueonInstanceType. - Block device mappings are merged with the image's (§ 07, algorithm 3;
ec2api/api/instance.py:944).UserDatais base64-decoded and passed to Nova (ec2api/api/instance.py:116).Placement.AvailabilityZoneis passed through. - 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, orNetworkInterface.N), with the combination rules ofec2api/api/instance.py:1234andec2api/api/instance.py:1318. Withdisable_ec2_classic, a request without subnet goes to the first subnet of the default VPC (ec2api/api/instance.py:1279). - A reservation ID
r-+ 8 characters of0-9a-zis 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 theiitem{reservation_id, launch_index, client_token?, disable_api_termination?}withvpc_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). - Failure on instance k: if k <
MinCount, everything created is rolled back (servers deleted, items removed, ports deleted); onceMinCountinstances 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]. - The response is the reservation as
DescribeInstancesformats it (ec2api/api/instance.py:175), instances typicallypending(Novabuilding).
- TerminateInstances (
ec2api/api/instance.py:186): refuses the whole call if one instance hasdisable_api_termination(OperationNotPermitted); otherwiseserver.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 withdelete_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 allowedvm_state(reboot: active, stopped, paused, suspended, error; stop: active, rescued, error; start: stopped), elseIncorrectInstanceState; thenreboot()(soft),stop(),start().Forceis ignored. The response is<return>true</return>, not the AWS state-change list (StopInstances/StartInstancesin_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>), orservers.getfor 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 fromOS-EXT-STS:vm_state(§ 07 algorithm 4);instanceType= flavor name orunknown;imageIdmapped fromimage.id(creating anamimapping if needed);placement.availabilityZone,keyName,launchTime= Novacreated,rootDeviceName, block devices from Cinder attachments (ec2api/api/instance.py:1529), kernel/ramdisk asaki/ari. VPC instances getnetworkInterfaceSet,vpcId,subnetId,groupSet,sourceDestCheck, private/public IP from the primary ENI; Classic instances get IPs from Novaaddressesand groups from Novasecurity_groups(ec2api/api/instance.py:766).privateDnsNameisOS-EXT-SRV-ATTR:hostnameor the IP (ec2_private_dns_show_ip,ec2api/api/instance.py:809). Admin contexts seekeyName (project, host)(ec2api/api/instance.py:817, never set by the API path [inferred]). Reservations group instances byreservation_idwithownerId= servertenant_id(ec2api/api/instance.py:302). - Attributes (
ec2api/api/instance.py:492,ec2api/api/instance.py:578,ec2api/api/instance.py:709): describe supportsblockDeviceMapping,disableApiTermination,groupSet,sourceDestCheck(VPC only),instanceType,kernel,ramdisk,rootDeviceName,userData(admin fieldOS-EXT-SRV-ATTR:user_data); others →InvalidParameterValue "Unknown attribute.". Modify supports exactly one ofdisableApiTermination(DB only),groupSet(VPC: Neutron port security groups of the single ENI),sourceDestCheck(VPC: portallowed_address_pairs),instanceType(only whenstopped: Nova resize, polling up to 45 s forresized, thenconfirm_resizeand up to 15 s more,ec2api/api/instance.py:670). Reset supportssourceDestCheckonly. - GetConsoleOutput / GetPasswordData (
ec2api/api/instance.py:479,ec2api/api/instance.py:464): Novaget_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 Glancecontainer_format:aki/kernel→aki,ari/ramdisk→ari, otherwiseami,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)."). WithoutNoRebootand with the server running, anamiitem withoutos_idis created at once and an eventlet green thread stops the server, polls it every second (up to 1 h) untilSHUTOFF, calls NovacreateImage(name, metadata={'ec2_id': <ami-id>}), stores the image UUID, and restarts the server; failure setsstate=failedon the item (ec2api/api/image.py:135). Otherwise the snapshot is synchronous. Default nameimage of <server uuid> at <time>.BlockDeviceMappingis ignored. - RegisterImage (
ec2api/api/image.py:198):ImageLocationwith no scheme ors3://→ S3 bundle import (§ 07 algorithm 6, asynchronous); another URL scheme → Glance imagecontainer_format=bare, disk_format=raw, visibility=privatethen Glanceweb-downloadimport of the URL (ec2api/api/image.py:285); no location butRootDeviceName→ an empty Glance image (upload of 0 bytes) carryingroot_device_name,bdm_v2=Trueand the JSONblock_device_mappingbuilt fromBlockDeviceMapping(volume sizes read from Cinder snapshots/volumes) (ec2api/api/image.py:239).Architecture,KernelId,RamdiskIdbecome 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 byget_public_items), Glanceimages.list()with the user token.ExecutableByandOwnerare 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 sameami-ID. Pending images (CreateImagein progress) are shown from the item withimageState=pendingorfailed(ec2api/api/image.py:453). State mappingec2api/api/image.py:94, S3 import statesec2api/api/image.py:835. - Attributes (
ec2api/api/image.py:492,ec2api/api/image.py:554,ec2api/api/image.py:642): describeblockDeviceMapping,description(from the item),launchPermission(group=allwhen public),kernel,ramdisk,rootDeviceName; others →InvalidRequest. ModifylaunchPermissionadd/remove for groupallonly → Glancevisibility=public/private;description→ item. ResetlaunchPermission→ 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):ClientTokenidempotency; Cindervolumes.create(size, snapshot_id, volume_type, availability_zone); itemvol; the Cinder volume'sdisplay_nameis set to the EC2 ID.Iops,Encrypted,KmsKeyIdare accepted and ignored. - AttachVolume (
ec2api/api/volume.py:75): Novavolumes.create_server_volume(server, volume, device); Nova 409/400 →UnsupportedOperation. Returns the attachment from Cinder withdeleteOnTermination=false. - DetachVolume (
ec2api/api/volume.py:96): finds the server from the Cinder attachment (theInstanceId,Device,Forceparameters are ignored); not attached →IncorrectState; Novadelete_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): Cindervolumes.list()(user), Nova servers[admin]fordeleteOnTermination. Statusreserved/attaching/detaching→in-use(ec2api/api/volume.py:195);attachmentSetfrom the first Cinder attachment. - CreateSnapshot (
ec2api/api/snapshot.py:32): volume status must beavailable,in-use,attachingordetaching; Cindervolume_snapshots.create(volume, force=True); itemsnap; snapshotdisplay_name= EC2 ID,display_description=Description. - DescribeSnapshots (
ec2api/api/snapshot.py:99): Cinder snapshots of the project; status mapec2api/api/snapshot.py:122(deletedsnapshots are hidden).OwnerandRestorableByare 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): Novaavailability_zones.list(detailed=False)minusinternal_service_availability_zone;ZoneName.1=verbosereturns the host/service tree (ec2api/api/availability_zone.py:175). - DescribeRegions (
ec2api/api/availability_zone.py:108): built from configuration only (region_listofname=host, else oneRegionOnewithec2_scheme://ec2_host:ec2_port ec2_path).RegionNameis ignored. - DescribeAccountAttributes (
ec2api/api/availability_zone.py:133):supported-platforms(EC2,VPCorVPConly),default-vpc(ID ornone),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), thevpcitem{cidr_block, is_default, route_table_id}withos_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 issg-<same 8 hex as the VPC>(ec2api/api/security_group.py:114).InstanceTenancyis ignored. - Default VPC (
ec2api/api/vpc.py:169): withdisable_ec2_classic=Trueand a non-admin context, the first describe of VPCs, subnets, route tables, security groups or internet gateways (and several other paths) creates a VPC172.31.0.0/16withis_default, an internet gateway attached to it, a subnet172.31.0.0/20and a0.0.0.0/0route to the gateway, under the file lockec2api-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);statealwaysavailable,dhcpOptionsIddefaults todefault(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 thesubnetitem, 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).AvailabilityZoneis 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);defaultForAzandmapPublicIpOnLaunchalwaysfalse.
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 makesCreateRouteidempotent, a different one →RouteAlreadyExists; the local route cannot be deleted. - Association IDs are derived, not stored:
rtbassoc-<hex of the subnet>for explicit associations andrtbassoc-<hex of the VPC>for the main association (ec2api/api/route_table.py:165,ec2api/api/route_table.py:520).ReplaceRouteTableAssociationon the main association changes the VPC's main table. - Route state:
activewhen the gateway is attached to the VPC or the ENI's instance isACTIVEin Nova, elseblackhole(ec2api/api/route_table.py:444);originCreateRouteTable,CreateRouteorEnableVgwRoutePropagation.DescribeRouteTablescalls 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 whendisable_ec2_classic) creates aneipallocitem{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 withoutAllowReassociation→Resource.AlreadyAssociated; Neutronupdate_floatingip(port_id, fixed_ip_address); association IDeipassoc-<hex of the allocation>. Classic associate: Novaadd_floating_ip(server, ip). - Release: VPC addresses need the allocation ID; an associated address →
InvalidIPAddress.InUse(withdisable_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).
| Path | Result |
|---|---|
/ 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-data | EC2 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 headersX-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 NSXX-Metadata-Provider+X-Forwarded-For(+ optionalX-Metadata-Provider-Signature), the instance then being found from Neutron ports[admin](ec2api/metadata/api.py:73). Missing headers → 400, bad signature → 403, noX-Forwarded-For→ 500 (EC2MetadataInvalidAddressis not caught specifically,ec2api/metadata/__init__.py:115). - Content (
ec2api/metadata/api.py:167): built by runningDescribeInstancesfor 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'suser_id,ec2api/metadata/api.py:221), anduser-data(base64-decodedOS-EXT-SRV-ATTR:user_data). The project of the instance must matchX-Tenant-ID(ec2api/metadata/api.py:155). - Cache: oslo.cache region, key
ec2api-metadata-<instance uuid>,cache_expiration15 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.