Plugged In
How Neutron wires a VM with OVN
You click Launch Instance
About a minute passes
You can ssh in
You click Launch Instance in the OpenStack dashboard, and about a minute later you can ssh in. This course is about the invisible half of that minute: how your VM got a working network cable.
scroll to begin ↓The Boot Story
One boot, one contract, and the single event that lets your VM start
First, a house handover
Booting a VM works like a new house handover. The builder has finished the house, but nobody gets the keys until the electrician issues the electrical safety certificate.
The builder: Nova
Assembles the machine (CPU, memory, disk), then stops at the front door and waits.
The electrician: Neutron
Wires the house, with OVN as its crew, and decides when the wiring is safe to switch on.
The certificate
One event, network-vif-plugged, sent from Neutron to Nova. No certificate, no keys, no boot.
Nova literally pauses the boot until Neutron sends network-vif-plugged. Everything in this course exists to produce that one event.
The boot, replayed as a group chat
Here is a real boot of a VM called web-1, replayed as a chat between the services. Step through it and notice who waits for whom.
When an instance is stuck in BUILD or dies with a 'vif plugging timeout', it is not a hypervisor problem. The certificate never arrived, and now you know exactly who to interrogate.
The whole story in seven steps
That chat is the entire course in miniature. Here is the same story as a map, with the module that unpacks each step.
"Create a port on network X for VM Y." The port is the VM's virtual network socket: MAC, IP and rules.
Module 3The port is recorded in Neutron's own database, then the desired state goes into the OVN Northbound database (the wishlist).
Module 3Nova schedules the VM to a compute host and tells Neutron "this port will live on host Z". Neutron records that decision, called binding.
Module 4On that host, Nova plugs a virtual cable (a TAP device) into the local Open vSwitch (OVS) bridge, tagged with the port's ID.
Module 5ovn-controller on that host recognises the tag, wires up the flows, and marks the port up in OVN.
Module 5Neutron sees the up signal, flips the port to ACTIVE, and fires network-vif-plugged at Nova.
Module 5Nova resumes the boot. The VM then gets its IP by DHCP and its config from the metadata service.
Module 6And Module 2? It introduces the full cast properly before the action starts.
Where the story starts in the code
Every boot begins with one web call to Neutron's REST API. The code that answers it lives in neutron/plugins/ml2/plugin.py, inside Neutron's ML2 core.
def create_port(self, context, port): self._before_create_port(context, port) result, mech_context = self._create_port_db(context, port) return self._after_create_port(context, result, mech_context)
Someone sent POST /v2.0/ports, meaning "please create a port". At boot time, that someone is Nova.
Beat one: check the request and prepare everything the new port will need (Module 3).
Beat two: write the port, MAC and IP included, into Neutron's own database (Module 3).
Beat three: hand the finished record to the backend driver (OVN) so real wiring can begin, and reply to the caller (Modules 3 to 5).
neutron/plugins/ml2/plugin.py · lines 1584 to 1587 · unmodified
Three scenarios from the field
No definitions, no memory tests. Use the story you just watched.
An instance sits in BUILD for five minutes, then lands in ERROR with 'vif plugging timeout'.
Which side failed to deliver?
You need to know a VM's IP address, but the instance is still booting.
When was that IP actually decided?
Three jobs, three workers. Which assignment matches what you watched in the chat?
Meet the Cast
In module 1, five names talked in a group chat. Here is who they actually are, where they run, and which databases they gossip through.
One council, many construction sites
Think of your cloud as a city council and its construction system. The control plane is the council building, where plans are lodged and work orders are drawn up. The data plane is the construction sites, one on every compute host, each with its own permanently stationed crew.
Click every box below. Notice how OVN intent only ever flows through databases, never direct phone calls.
Control plane · the council building (controller nodes)
Data plane · one construction site (each compute host)
× this whole crew is repeated identically on compute-1, compute-2, … compute-N
The full cast list
Seven characters, two homes: the controller nodes, and every compute host. When something breaks, the first question is always "whose job was that?".
neutron-server
The council counter. Runs the REST API and the ML2 plugin, with the OVN driver inside, and owns the Neutron SQL database, the source of truth for the API.
Runs on: controller
ML2
A plugin framework inside neutron-server. It lets interchangeable mechanism drivers implement networks and ports; here the driver is OVN.
Runs on: controller (inside neutron-server)
OVN Northbound DB
The blueprint registry: desired state in networking terms, logical switches, logical switch ports, DHCP options and ACLs. Neutron writes here.
Runs on: controller
ovn-northd
The engineering office. A daemon that translates NB blueprints (intent) into SB work orders (logical flows).
Runs on: controller
OVN Southbound DB
The job board: instructions plus live inventory. Every hypervisor is registered here as a chassis, and port-to-chassis bindings live here.
Runs on: controller
ovn-controller
The local crew. Reads SB, programs the local Open vSwitch with real flows, claims ports when their TAP device appears, and reports the heartbeat Neutron displays as the "OVN Controller agent".
Runs on: every compute host
OVN Metadata agent
The site concierge (neutron/agent/ovn/metadata/agent.py). Builds a tiny network namespace per network so VMs can reach the metadata service.
Runs on: every compute host
Compared with classic ML2/OVS, two regulars are missing: neutron-dhcp-agent (a dnsmasq per network) and neutron-openvswitch-agent on every host. OVN replaces both with flows.
They do not phone each other. They share two databases.
Classic Neutron backends shouted commands over an RPC message queue and hoped an agent heard them. With OVN, Neutron mostly writes desired state into the NB database, and a pipeline of daemons makes reality match.
Watch one intent travel the pipeline. Neutron wants a new ACL to exist:
neutron-server writes the ACL row into NB
NB DB now records the intent
ovn-northd translates it into logical flows
SB DB publishes the work orders
every ovn-controller reprograms its local OVS
Neutron does not send OVN a command and hope; it writes a row in a database. If any daemon restarts, it simply re-reads the database and catches up. That makes the whole pipeline idempotent by design.
The council checks the crew's pulse
Neutron treats each host's ovn-controller as an agent, backed by its SB heartbeat. Before it will bind a port to a host, the OVN driver asks two questions: is a crew registered there, and is it alive?
neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
agents = n_agent.AgentCache().get_agents(
{'host': bind_host,
'agent_type': ovn_const.OVN_CONTROLLER_TYPES})
if not agents:
LOG.warning('Refusing to bind port %(port_id)s due to '
'no OVN chassis for host: %(host)s',
{'port_id': port['id'], 'host': bind_host})
return
agent = agents[0]
if not agent.alive:
LOG.warning("Refusing to bind port %(pid)s to dead agent: "
"%(agent)s", {'pid': context.current['id'],
'agent': agent})
return
Ask the agent list (kept fresh from the Southbound database): which crews are registered for the host this port should land on?
Only one kind of crew counts here: agents of the OVN controller type.
Is no crew registered on that site at all?
Then note it in the log: refusing to bind this port, because that host has no OVN chassis.
And walk away. Nova will see the port fail to bind.
A crew exists. Take it, and now check its pulse.
Has its heartbeat gone stale? A registered but silent crew counts as dead.
Log it and refuse again. Never hand a port to a site where nobody answers.
The full binding story, including what Nova does with a refusal, is module 4.
$ openstack network agent list
| OVN Controller agent | compute-7 | :-) | UP |
| OVN Metadata agent | compute-7 | :-) | UP | # XXX means the heartbeat stopped
# the same crews as OVN sees them: one chassis per host, with its tunnel endpoint
$ ovn-sbctl show
Chassis "…" hostname: compute-7 Encap geneve ip: "10.0.0.7" Port_Binding …
# a host missing here, or marked XXX above, will refuse every port binding.
# that story, and the log lines it leaves, is module 4
Your turn on the tools
Three situations. Use the cast, not your memory of the words.
Every VM on compute-12 has lost networking. VMs on every other host are fine. Which cast member do you interrogate first?
DHCP is misbehaving on your OVN cloud. You go hunting for the DHCP agent process to restart, and find none. Why?
Neutron wants a new ACL to exist. Which path does that intent travel?
Cast introduced. Next, module 3: A Port Is Born, where we follow one port-create call from the council counter all the way into the Northbound registry.
A Port Is Born
The two-phase moment a virtual network card gets its identity papers, long before any wire exists
The Registry Office
You met the cast in Meet the Cast; now watch them handle their first piece of business together. Your VM's IP address was decided before the VM existed, and this is the moment it happened.
When Nova requests a network card for the new VM, Neutron acts like a civil registry issuing a birth certificate. The identity papers go into Neutron's own ledger first; the physical wire comes much later.
Port ID
The certificate number: a UUID that identifies this port everywhere, forever.
MAC address
The fingerprint: the MAC address is the hardware-level identity of the virtual network card.
IP address
The registered home address, reserved in the ledger before any wire exists.
Status: DOWN
Born switched off, on purpose. An IOU must be repaid before it may go ACTIVE.
One Request, Two Ledgers
Follow one POST /v2.0/ports request from Nova all the way in and back out. Count the writes: there are two, into two different databases, and everything in this module hangs on the gap between them.
Notice the ending: Nova already holds the MAC and IP, yet the port's status is DOWN. Identity first, wiring later.
Phase One: Inside the Ledger
Phase one is the
precommit
hook, which the
ML2
plugin runs inside the very
SQL
transaction
that records the port. Everything here is
atomic:
if any line fails, the whole certificate is torn up. Straight from neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:
port = context.current if ovn_utils.is_lsp_ignored(port): return ovn_utils.validate_and_get_data_from_binding_profile(port) self._validate_port_extra_dhcp_opts(port) if self._is_port_provisioning_required(port, context.host): self._insert_port_provisioning_block(context.plugin_context, port['id']) ovn_revision_numbers_db.create_initial_revision( context.plugin_context, port['id'], ovn_const.TYPE_PORTS, std_attr_id=context.current['standard_attr_id'])
Pick up the application form: the port exactly as the caller described it.
A few exotic ports are none of OVN's business; those skip the registry entirely.
Check the paperwork is genuine: the binding profile first...
...then any custom DHCP options the request carried.
Will this port one day need a real wire on a host? Then file the IOU (the provisioning block) that stops it being declared ACTIVE before the wiring exists.
Finally, open a revision number at version 0: the little counter that keeps the ledger and the blueprints honest with each other.
status: DOWN
How every port is born; it says nothing about failure.
provisioning block
The IOU: ACTIVE is withheld until OVN reports a live wire.
revision_number: 0
The sync counter that lets Neutron detect and repair drift between the two databases.
Anything that must live or die with the database write happens inside the transaction (precommit). Slow conversations with external systems happen after it commits, in postcommit, with revision numbers standing by to repair any gap. You will meet this pattern in almost every distributed system.
Phase Two: Publishing the Blueprint
The instant the ledger commits, ML2 calls the postcommit hook. There, the OVN mechanism driver photocopies the record and hands it to the OVN client, its clerk for the blueprint registry:
port = copy.deepcopy(context.current) port['network'] = context.network.current self._ovn_client.create_port(context.plugin_context, port) self._notify_dhcp_updated(port['id'])
Photocopy the record; the original stays safely in the ledger.
Staple the network details onto the copy...
...and hand the bundle to the OVN client, the clerk who writes to the blueprint registry.
Then let the DHCP machinery know a new resident is on the way.
The clerk now creates the port's twin, a
Logical_Switch_Port,
in the
NB database.
From neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py, original comment and all:
with self._nb_idl.transaction(check_error=True) as txn: dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options( port_info, txn=txn) # The lport_name *must* be neutron port['id']. It must match the # iface-id set in the Interfaces table of the Open_vSwitch # database which nova sets to be the port ID. kwargs = { 'lport_name': port['id'], 'lswitch_name': lswitch_name, 'addresses': port_info.addresses, 'external_ids': external_ids, 'parent_name': port_info.parent_name, 'tag': port_info.tag, 'enabled': port.get('admin_state_up'), 'options': port_info.options, 'type': port_info.type, 'port_security': port_info.port_security, 'dhcpv4_options': dhcpv4_options, 'dhcpv6_options': dhcpv6_options }
Open one atomic transaction against the NB database, through the IDL. Everything below lands together, or not at all.
Prepare the DHCP settings for IPv4 and IPv6 inside that same transaction.
The authors left a treasure map in this comment: the logical port MUST carry the Neutron port ID as its name, because Nova stamps that exact ID on the virtual cable (its iface-id).
Now assemble the birth announcement, field by field:
the shared name, which is the Neutron port ID...
...the logical switch (the network) it lives on...
...the MAC and IP identity papers...
...a Neutron return address written onto the OVN record...
...fine print for nested setups, where one port rides inside another...
...whether the port is administratively switched on...
...backend options and the port type...
...the port security anti-spoofing setting...
...and the DHCP settings prepared a moment ago.
The logical port is named with the Neutron port ID, and Nova stamps that very same ID on the virtual cable as its iface-id in Open vSwitch. That shared name is the thread the whole boot story hangs on, and module 4 starts pulling it.
The Gatehouse: Security Groups Become Port Groups
There is no iptables firewall on the hypervisor here. A security group becomes an OVN port group, and its rules become ACLs attached to that group in the NB database.
Think of a gated estate: a new resident starts on the no-visitors list, and each security group is a guest list that opens the gate to named callers.
The untrusted newcomer joins the default drop group: every packet refused
It then joins one port group per security group it belongs to
ACLs on those groups punch the explicit allow holes
Still inside the very same NB transaction, in ovn_client.py:
sg_ids = utils.get_lsp_security_groups(port) # If this is not a trusted port and port security is enabled, # add it to the default drop Port Group so that all traffic # is dropped by default. if not utils.is_lsp_trusted(port) and port_info.port_security: self._add_port_to_drop_port_group(port_cmd, txn) # Just add the port to its Port Group. for sg in sg_ids: txn.add(self._nb_idl.pg_add_ports( utils.ovn_port_group_name(sg), port_cmd))
Which security groups is this port a member of?
The policy is spelled out in the original comment: deny by default.
Unless the port is specially trusted, and as long as port security is on, enrol it in the default drop group. Every packet is refused until a rule says otherwise.
Then the memberships themselves:
for each security group, add the port to the matching OVN port group, in the same NB transaction. The allow rules hang off those groups as ACLs.
$ openstack port show $PORT_ID -c status -c mac_address -c fixed_ips -c binding_host_id
| DOWN | fa:16:3e:… | 192.168.10.5 | (no host yet) |
# the ledger side: the IOU and the sync counter that precommit filed
mysql> SELECT pb.entity FROM neutron.provisioningblocks pb JOIN neutron.ports p ON pb.standard_attr_id=p.standard_attr_id WHERE p.id='$PORT_ID'; → L2
mysql> SELECT revision_number FROM neutron.ovn_revision_numbers WHERE resource_uuid='$PORT_ID'; → 0
# the blueprint side: the twin exists, named with the port ID, not yet up
$ ovn-nbctl list Logical_Switch_Port $PORT_ID
| up: false | addresses: ["fa:16:3e:… 192.168.10.5"] | port_security: [...] |
# the gatehouse: the security group's rules, living as ACLs on its port group
$ ovn-nbctl acl-list pg_$SG_ID # dashes in the SG UUID become underscores
# port in the ledger but no twin in NB? phase two failed: check neutron-server.log.
# the revision counter you just saw is what the repair machinery compares
Prove It in the Field
Four situations straight from real operations. Apply what you have just seen; each answer comes with the why.
You run openstack port list and the port is there, but
ovn-nbctl
shows no Logical_Switch_Port for it. Which half of create_port failed?
A port you created a second ago reports status DOWN even though the API call succeeded. What is going on?
You are designing a system like this from scratch. Why must the Logical_Switch_Port be named with the Neutron port ID rather than any random string?
A packet is being dropped and you suspect a security group rule. Under OVN, where do those rules actually live?
Next: the port has papers but no home; in Finding a Home, Neutron picks the compute host it will live on and binding moves it in.
Finding a Home
Nova proposes a host for the new port; Neutron inspects the berth and can refuse. Binding is that negotiation, stamped.
A Ship Without a Berth
A Port Is Born left you holding fresh papers: a port with an ID, a MAC and an IP, status DOWN, and a provisioning block IOU promising a real wire. Now the port needs somewhere to live.
A port with no host is a passport with no address. Binding is Neutron approving the address Nova picked, and it can say no.
Picture a harbourmaster assigning a ship to a berth. Six characters run this port town:
The ship
Your VM, cargo and all, waiting off shore until its port has a home.
The papers
The port from module 3: genuine, stamped, and completely addressless.
The berth
A compute host, compute-7 in our story, proposed by Nova's scheduler.
The dock crew
That host's ovn-controller, registered as a chassis. No crew, no mooring.
The harbourmaster
ML2 and its OVN mechanism driver, who confirm the berth, or refuse it.
The mooring plan
The confirmation itself: vif_type plus vif_details, how to tie the ship up, in writing.
The Negotiation, End to End
Watch one PUT request turn a homeless port into a bound one. Every stop on this route is a place where binding can die.
Notice who does the very last step. Neutron only stamps paperwork; the physical plugging is done by Nova through os-vif, which pushes a TAP device into br-int.
A Negotiation with Retries
The world keeps moving while the harbourmaster thinks: hosts die, live migrations land, other API calls touch the same port. So ML2 treats binding as optimistic and wraps it in a retry loop rather than trusting one attempt.
neutron/plugins/ml2/plugin.py
for count in range(1, MAX_BIND_TRIES + 1): if count > 1: # yield for binding retries so that we give other threads a # chance to do their work greenthread.sleep(0) # multiple attempts shouldn't happen very often so we log each # attempt after the 1st. LOG.info("Attempt %(count)s to bind port %(port)s", {'count': count, 'port': context.current['id']})
Try to find a berth, up to a fixed maximum number of attempts (MAX_BIND_TRIES).
Is this a second or later attempt? Then two courtesies apply.
First, as the original comment says, step aside for a moment...
...so other work can finish. Whatever spoiled the last attempt may clear itself.
That pause is a greenthread yield: blink, and let the room breathe.
Second, retries should be rare, so each one is worth recording.
Write the attempt number and the port into the log, where an operator can grep for it later.
Nova proposes binding:host_id
Each mechanism driver gets a chance to stamp
Stamped? Commit the binding and reply
Nobody stamped? Loop again; after MAX_BIND_TRIES the port is marked binding_failed
Three Ways to Say No
Before stamping anything, the OVN driver runs the berth checklist: right cargo type, crew on site, berth equipped. Three questions, three refusals, and each one leaves a plain log line you will grep for on a bad night.
Check 1, the cargo type: neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
port = context.current vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) if vnic_type not in self.supported_vnic_types: LOG.debug('Refusing to bind port %(port_id)s due to unsupported ' 'vnic_type: %(vnic_type)s', {'port_id': port['id'], 'vnic_type': vnic_type}) return
Pick up the application: the port exactly as submitted.
What kind of plug does this cargo want? The vnic_type field says; assume a normal one if unstated.
Is it a kind this driver does not handle, such as some SR-IOV direct-hardware plugs?
Note the refusal in the log, politely and precisely...
...naming the port and the plug type it asked for...
...and walk away without stamping.
A driver that returns without calling set_binding is saying I abstain. Another mechanism driver may still bind the port; only when nobody stamps does the port end up binding_failed.
Check 2 is the crew check you watched in Meet the Cast: is a chassis registered for the host, and is its heartbeat alive? A registered but silent crew counts as dead, and each refusal has its own log line:
no OVN chassis for host: %(host)s
Nobody is registered to work that berth: the
Southbound database
holds no chassis for the host.
Refusing to bind port ... to dead agent
A crew is registered but its heartbeat has gone stale. No ship is moored where nobody answers.
Check 3 is the equipment. Networks arrive as segments, and flat and VLAN segments ride a named physnet. The host's bridge mappings must include that physnet, or this berth simply lacks the right crane:
Check 3, the equipment: neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
if ((network_type in [const.TYPE_FLAT, const.TYPE_VLAN]) and (physical_network not in chassis_physnets)): LOG.info('Refusing to bind port %(port_id)s on ' 'host %(host)s due to the OVN chassis ' 'bridge mapping physical networks ' '%(chassis_physnets)s not supporting ' 'physical network: %(physical_network)s', {'port_id': port['id'], 'host': bind_host, 'chassis_physnets': chassis_physnets, 'physical_network': physical_network})
Only flat and VLAN segments touch a named physical fabric, so only they face this check.
Is that fabric missing from the physnets this chassis declares in its bridge mappings?
Then refuse, and say exactly why in the log...
...naming the port, the host and the mismatch in full...
...including the complete list of physnets the host does support...
...next to the one the network needs.
An operator can compare the two lists at a glance.
That log line has ended a thousand mysteries about why network prod will not bind on some hosts.
The Stamp and the Mooring
Every check passed, so the harbourmaster writes the mooring plan and stamps it. This is the hero moment of the whole negotiation:
neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
else: vif_type = portbindings.VIF_TYPE_OVS vif_details = copy.deepcopy(self.vif_details[vif_type]) ovn_bridge = ovn_utils.get_ovn_bridge_from_chassis_private( agent.chassis_private) dp_type = ovn_utils.get_datapath_type(bind_host, self.sb_ovn) vif_details.update({ portbindings.VIF_DETAILS_BRIDGE_NAME: ovn_bridge, portbindings.OVS_DATAPATH_TYPE: dp_type, }) context.set_binding(segment_to_bind[api.ID], vif_type, vif_details) break
In the everyday case, the mooring plan is type ovs: plug into Open vSwitch.
Start from the standard details sheet for that plan, on a fresh photocopy so edits never leak back into the template.
Ask the chassis record which integration bridge this host uses, usually br-int...
...and which datapath flavour its switch runs, looked up via the Southbound database.
Write both onto the plan Nova will read: the bridge to plug into...
...and the datapath type, so Nova builds the right kind of plug.
The stamp itself: set_binding records the segment, the vif_type and the details. This line IS the binding.
Berth confirmed; stop trying other segments.
Nova reads three things off the stamped plan, then does its side of the bargain on the host:
vif_type: ovs
The wiring method: this is an Open vSwitch mooring, not a hardware one.
bridge_name: br-int
Which patch panel on the host the cable belongs in.
datapath_type
Kernel or userspace switching, so the plug matches the socket.
A TAP device appears on compute-7: one end for the VM, one end loose.
The exact bridge the vif_details named. Neutron never touches the wire; this is all Nova.
The plugged interface is stamped with external-ids:iface-id = the Neutron port ID.
The only glue between Nova's plugged cable and OVN's paperwork is a string equality: iface-id on the interface equals the Logical_Switch_Port name equals the Neutron port ID. No callbacks, no shared lock, just a name tag; cheap, robust, and the entire reason module 5's magic works.
$ openstack port show $PORT_ID -c binding_host_id -c binding_vif_type -c binding_vif_details -c status
| compute-7 | ovs | bridge_name: br-int, datapath_type: system | DOWN |
# the berth checklist, runnable before or after the fact. crew registered and alive?
$ openstack network agent list --host compute-7 → OVN Controller agent | :-) | UP
# berth equipped? the bridge mappings must carry the network's physnet
$ ovn-sbctl --columns=hostname,other_config find Chassis hostname=compute-7
| other_config: {ovn-bridge-mappings="datacentre:br-ex", ovn-cms-options=…} |
# Nova's side of the bargain, on compute-7: the cable and its name tag
$ ovs-vsctl --columns=name,external_ids find Interface external_ids:iface-id=$PORT_ID
| name: tap3f2a… | external_ids: {iface-id=$PORT_ID, …} |
# vif_type says binding_failed instead? the refusal is named in the log
$ grep "Refusing to bind port" neutron-server.log
Prove It on the Docks
binding_failed is one of the most common boot failures you will ever meet. Match each symptom to its most likely cause; the chips are the causes.
vif_type = binding_failed on every host you try
Binding fails only on compute-9; every other host is fine
Binding fails only for the VLAN network named prod; other networks bind fine on the same host
Binding refused for a port requesting direct hardware access (SR-IOV style)
Now three situations from the field. Think like the harbourmaster.
openstack port show reports binding_vif_type=binding_failed with the host set to compute-3. Which three checks does this module hand you?
Nova's scheduler already weighed CPU, memory and disk when it chose compute-3. Why does Neutron still get the final say on the placement?
set_binding has succeeded and the API reply is on its way back. Who now physically creates the TAP device and plugs it into br-int?
Next: the cable is plugged and name-tagged, yet the port still says DOWN; in The Wake-Up Call, ovn-controller on compute-7 spots that tag, wires up the flows, and everyone finds out the ship has arrived.
The Wake-Up Call
Where network-vif-plugged is born, and every hop it takes from a compute host back to the boot Nova paused
Mission Control Runs a Go/No-Go Poll
Finding a Home ended with the TAP plugged into br-int and name-tagged with the port ID, but nobody has said power on. Nova is still holding the boot of web-1, phone in hand.
Neutron runs this moment the way mission control runs a rocket launch: a go/no-go poll. Every station must call GO, and until the checklist is empty, launch status stays red and the port stays out of ACTIVE.
Mission control
neutron-server, keeper of the launch checklist and the only one allowed to phone the customer.
The checklist
The provisioning blocks on the port. One entry per station that has not yet reported GO.
The pad crew
ovn-controller on compute-7, the crew standing at the rocket who can see the actual wiring.
The customer
Nova, boot paused, waiting for one call: network-vif-plugged.
The station was added to the checklist back in module 3, at the moment of port creation. From neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:
def _insert_port_provisioning_block(self, context, port_id): # Insert a provisioning block to prevent the port from # transitioning to active until OVN reports back that # the port is up. provisioning_blocks.add_provisioning_component( context, port_id, resources.PORT, provisioning_blocks.L2_AGENT_ENTITY )
The registrar files the IOU while the port is being created.
The original comment is the whole plot of this module: hold the port out of ACTIVE until OVN reports it up.
Add one station to the checklist for this port...
...named L2, the wiring crew. This module is that station finally calling GO.
Five Links, Zero Polling
Nothing in this chain polls. Every link is event-driven: each hop wakes the next one the instant something real changes.
Step through the five links. Each one has a symptom when it breaks, which makes this animation your map for bisecting a vif plugging timeout.
Port_Binding.chassis
The claim: the
chassis
field on the
Port_Binding
row names the host that took the port.
LSP.up = true
The proof: flows are programmed, so the logical port is declared up.
The Subscription: Wake Me Only for the Flip
neutron-server never asks the database "is it up yet?". It lodges a subscription through OVSDB: wake me when a row in the Logical_Switch_Port table flips from DOWN to UP.
The handler class is called LogicalSwitchPortUpdateUpEvent, and its own documentation says it plainly: "This happens when the VM goes up". From neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:
def match_fn(self, event, row, old): if not (utils.is_lsp_up(row) and utils.is_lsp_enabled(row)): return False if hasattr(old, 'up') and not utils.is_lsp_up(old): # The port has transitioned from DOWN to UP, and the admin state # is UP (lsp.enabled=True) return True if hasattr(old, 'enabled') and not utils.is_lsp_enabled(old): # The user has set the admin state to UP and the port is UP too. return True return False def run(self, event, row, old): self.driver.set_port_status_up(row.name)
The filter. Every change to the table is offered here, with the row as it is now and how it looked before (old).
First gate: unless the port is now up AND its admin state is on, this change is noise. Hang up.
Was the up flag false a moment ago? Then this is the genuine DOWN to UP flip we subscribed for; the original comment spells it out.
Match. Wake the handler.
Second path: the admin switch was just flipped on while the port was already up. That counts as the same moment.
Match. Wake the handler.
Anything else: not our moment, back to sleep.
Only matched changes ever reach the action.
Hand the port name (which IS the Neutron port ID, thanks to module 3) to the driver: make the GO call.
OVSDB row-update notifications mean a database write on a compute host wakes a Python handler inside neutron-server, with no queue and no polling anywhere. The subscription is the filter, and match_fn is that filter written out.
GO, and the Checklist Empties
The GO call lands in the driver, back in mech_driver.py. Two jobs: log the moment for posterity, then tell the checklist the L2 station has reported.
def set_port_status_up(self, port_id): # Port provisioning is complete now that OVN has reported that the # port is up. Any provisioning block (possibly added during port # creation or when OVN reports that the port is down) must be removed. LOG.info("OVN reports status up for port: %s", port_id) self._update_dnat_entry_if_needed(port_id) admin_context = n_context.get_admin_context() provisioning_blocks.provisioning_complete( admin_context, port_id, resources.PORT, provisioning_blocks.L2_AGENT_ENTITY)
The GO call arrives carrying one thing: the port ID.
The comment gives the mood: the wire is real now, so the hold must be lifted.
Print the log line every operator lives by. If this line exists, the whole OVN half of the chain worked.
Housekeeping: refresh the address-translation entry if this port carries a floating IP.
Pick up the master key: this runs with admin rights, on behalf of the system itself.
Then ring the checklist desk: the L2 station formally reports its work complete for this port.
grep "OVN reports status up" neutron-server.log is the single fastest way to bisect a vif plugging timeout. Line present: interrogate the checklist and the call to Nova. Line absent: interrogate the compute host and OVN.
(grep searches the log for that exact phrase.)
The checklist desk itself lives in neutron/db/provisioning_blocks.py, and it only ever announces a launch when the list is empty:
# now with that committed, check if any records are left. if None, emit # an event that provisioning is complete. if pb_obj.ProvisioningBlock.objects_exist( context, standard_attr_id=standard_attr_id): return LOG.debug("Provisioning complete for %(otype)s %(oid)s triggered by " "entity %(entity)s.", log_dict) registry.publish(object_type, PROVISIONING_COMPLETE, entity, payload=events.DBEventPayload( context, resource_id=object_id))
One station just reported GO. The original comment asks the launch question: is anyone still holding?
Look down the checklist for this port...
...any station still listed means no launch. Leave quietly, status unchanged.
Checklist empty: note it in the flight log...
...and announce PROVISIONING_COMPLETE over the internal loudspeaker. The listener flips the port status to ACTIVE.
L2_AGENT_ENTITY
The wiring station: cleared when OVN reports the port up. This module.
DHCP_ENTITY
The address station: some setups also make
DHCP
call GO. ACTIVE waits for every station on the list, not just one.
The Call Nova Has Been Waiting For
The status flip is what dials the phone. Neutron's Nova notifier watches port status changes and, in neutron/notifiers/nova.py, keeps a strict rule about which flip means "plugged":
# We only notify nova when a vif is plugged which only occurs # when the status goes from: # NO_VALUE/DOWN/BUILD -> ACTIVE/ERROR. elif (previous_port_status in [sql_attr.NO_VALUE, constants.PORT_STATUS_DOWN, constants.PORT_STATUS_BUILD] and current_port_status in [constants.PORT_STATUS_ACTIVE, constants.PORT_STATUS_ERROR]): event_name = VIF_PLUGGED
The rule in the authors' own words: only one kind of status change means a plug went live.
The port has never been up before: brand new, DOWN, or still in BUILD...
...and it has just landed on ACTIVE (or ERROR, so Nova is never left waiting on a lost cause)...
...that flip, and only that flip, is stamped network-vif-plugged.
The notifier holds queued events for a moment (batching), then sends one POST to Nova's external events API. Module 1 opened with Nova waiting by the phone; here is the call being made, hop by hop.
That is the whole course in one phone call. The cliffhanger from module 1 is closed: the certificate arrived, and Nova stops pacing by the phone.
$ ovn-sbctl --columns=chassis find Port_Binding logical_port=$PORT_ID
chassis: <uuid> # empty? nobody claimed it: check the TAP, its iface-id, and ovn-controller
$ ovn-sbctl get Chassis <uuid> hostname → compute-7
# link 2, the proof: did the logical port flip up?
$ ovn-nbctl get Logical_Switch_Port $PORT_ID up → true
# link 3, the event, heard by mission control
$ grep "OVN reports status up for port: $PORT_ID" neutron-server.log
# link 4, the checklist empties and the port goes ACTIVE
mysql> SELECT pb.entity FROM neutron.provisioningblocks pb JOIN neutron.ports p ON pb.standard_attr_id=p.standard_attr_id WHERE p.id='$PORT_ID'; → Empty set
$ openstack port show $PORT_ID -c status → ACTIVE
# link 5, the phone call: did Nova take it and resume the boot?
$ grep "network-vif-plugged" nova-compute.log # on compute-7
$ openstack server event list $SERVER_ID # the create action journals the wait
# the first command that disappoints you names the broken link. fix there, not downstream
Prove You Can Bisect It
Four timeouts and design questions from the field. Each one is a link in the chain; find it.
An instance died with a vif plugging timeout. You grep the neutron-server log for "OVN reports status up" and the line IS there for your port. Which half of the chain do you interrogate?
Same timeout, different port: this time the SB Port_Binding row for the port has an empty chassis. Where do you look now?
Binding succeeded back in module 4, so Neutron already knows exactly where the port lives. Why not flip it to ACTIVE right there and skip all this machinery?
A port carries two provisioning blocks, DHCP and L2. OVN reports the port up and the L2 station calls GO. What is the port status now, and what does Nova hear?
Next: web-1 is powering on, and its first words are two questions: what is my address, and what is my config? First Breath follows DHCP and metadata, both answered the OVN way.
First Breath
A DHCP answer with no server and a metadata call with no password: the VM's first two questions, and the machinery that answers them
Checked In, Phone in Hand
The Wake-Up Call ended with the certificate: network-vif-plugged landed, Nova resumed the boot, and inside the VM an operating system is now starting for the very first time.
Think of a hotel guest who has just stepped into their room. Their first two acts: ask the building who they are on this network, then ring guest services to find out who they are supposed to become.
Who am I here?
DHCP. The key card was programmed at booking time (module 3); the building's own wiring hands over the room number the moment the guest asks. No receptionist is woken up.
Who do I become?
Metadata. Dial 9 from the room phone; the switchboard knows which room is calling and fetches the welcome pack: hostname, ssh keys, first-run instructions.
Neither act involves a DHCP server process, and the VM holds no passwords or credentials at all. Both work anyway, and this module shows you how.
The Room Number Is in the Wiring
Older Neutron backends ran a dnsmasq process per network to answer these questions. Under OVN that job is abolished: there is no daemon, only data compiled into switch rules.
The guest asks aloud: the VM broadcasts a DHCP discover
The wiring answers: OVS flows on the same hypervisor reply with IP, gateway, MTU and DNS
Nothing leaves the building: no packet crosses to another host, no server process exists
Where did the answers come from? Neutron computed them when the port was born, and they landed in the
NB database's
DHCP_Options table. Here is the computation, from ovn_client.py:
default_lease_time = str(ovn_conf.get_ovn_dhcp_default_lease_time()) mtu = network['mtu'] options = { 'server_id': service_id, 'lease_time': default_lease_time, 'mtu': str(mtu), }
Ask Neutron's own settings how long guests may trust the answer (the lease time).
Read the parcel-size limit, the MTU, straight off the network record. No host is consulted.
Program the key card. These three values are the entire answer:
who the reply appears to come from...
...how long it stays valid...
...and the parcel-size limit. Notice what is missing: there is no server here to start, crash or restart.
neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py · lines 2332 to 2338 · unmodified
And the Enable DHCP tick-box in the dashboard? In the same file, it is one if statement deciding whether the options box gets filled:
dhcp_options = {'cidr': subnet['cidr'], 'options': {}, 'external_ids': external_ids} if subnet['enable_dhcp']: if subnet['ip_version'] == const.IP_VERSION_4: dhcp_options['options'] = self._get_ovn_dhcpv4_opts( subnet, network, server_mac=server_mac) else: dhcp_options['options'] = self._get_ovn_dhcpv6_opts( subnet, server_id=server_mac)
Start an answer sheet for this subnet: its address range (the CIDR) on the front, the options box empty, plus a Neutron return address.
Here is the Enable DHCP tick-box, living its true life as one if statement.
IPv4 subnet? Fill the options box with the values you just saw being computed...
...IPv6? Fill in the equivalent instead.
Untick the box and the options simply stay empty. Nothing is stopped, killed or restarted.
neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py · lines 2279 to 2288 · unmodified
To change what guests are told, change the network record. Neutron rewrites the DHCP_Options row,
northd
recompiles it, and
ovn-controller
on every hypervisor answers with the new values. There is nothing to restart.
Dialling 9: The Metadata Call
Room number sorted, cloud-init picks up the room phone: a plain HTTP request to 169.254.169.254, the same magic number on every cloud in the world.
The call carries no password and no token, yet it must be answered with secrets like your ssh keys. Step through how the switchboard pulls that off; the machinery behind each actor comes next.
The welcome pack is what makes the VM yours: hostname, your ssh public key, and your user-data script. When ssh just works a minute after launch, this call is why.
The Switchboard Room
Who built that room? Every compute host runs an OVN metadata agent, and for each network with a VM on the host it fits out one switchboard room, made of three parts:
ovnmeta-<network-id>
A private room with its own phone line, so identical addresses on different networks never meet
Two wall sockets joined through the wall: one end inside the room, the other on the tenant network
Listens on 169.254.169.254 port 80 inside the room and forwards every call to the agent
Here is the room being fitted out, from agent.py, original comments and all:
LOG.info("Provisioning metadata for network %s", net_name) # Create the VETH pair if it's not created. Also the add_veth function # will create the namespace for us. namespace = self._get_namespace_name(net_name) veth_name = self._get_veth_name(net_name) ip1 = ip_lib.IPDevice(veth_name[0]) if ip_lib.device_exists(veth_name[1], namespace): ip2 = ip_lib.IPDevice(veth_name[1], namespace) else: LOG.debug("Creating VETH %s in %s namespace", veth_name[1], namespace) # Might happen that the end in the root namespace exists even # though the other end doesn't. Make sure we delete it first if # that's the case. if ip1.exists(): ip1.link.delete() ip1, ip2 = ip_lib.IPWrapper().add_veth( veth_name[0], veth_name[1], namespace)
Announce in the log: fitting out the switchboard room for this network.
The original comment gives the game away: creating the cable also creates the room.
Work out the room's name (ovnmeta plus the network ID) and names for the two ends of the cable.
Take hold of the end that will live out in the corridor, the host's main network space.
If the in-room end already exists, reuse it rather than rewiring...
...otherwise note in the log that a fresh cable is going in.
Half-finished wiring happens in the real world; the comment admits it.
So clear away any stale corridor end first...
...then run the pair properly: one end in the corridor, one inside the room, and the room itself springs into existence along with it.
neutron/agent/ovn/metadata/agent.py · lines 770 to 788 · unmodified
How did the agent know a VM had landed here? Not by polling. It subscribed to row events on the
SB database's
Port_Binding table, exactly the way neutron-server heard up=true in module 5. One architectural idea, event-driven database watching, reused everywhere in OVN land.
The Signature That Opens Doors
One puzzle remains: the request that reaches the agent is anonymous. The switchboard identifies the caller by provenance: the call arrived in network X's room from a particular source IP, and Neutron's records map that pair to exactly one port and one instance.
Then the agent vouches for the caller in writing. From server.py, the note it staples to the request before forwarding it:
headers = { 'X-Forwarded-For': req.headers.get('X-Forwarded-For'), 'X-Instance-ID': instance_id, 'X-Tenant-ID': tenant_id, 'X-Instance-ID-Signature': common_utils.sign_instance_id( self.conf, instance_id) }
Write the covering note that rides to Nova as HTTP headers.
Keep the original caller's address on file...
...name the instance that is calling. The agent worked this out from the port; the VM never introduced itself.
...record which tenant (project) owns it...
...and sign the instance ID with an HMAC, using a secret the agent shares with Nova.
The VM cannot forge this: it never sees the secret. Nova recomputes the signature, and a mismatch is refused.
neutron/agent/ovn/metadata/server.py · lines 141 to 147 · unmodified
X-Instance-ID
Who is calling, established from network location, never from the caller's own claims.
X-Instance-ID-Signature
The HMAC proof that a real metadata agent, not a mischievous guest, named that instance.
metadata_proxy_shared_secret
The secret both sides must agree on. If Neutron and Nova disagree, every signature fails and metadata returns
403 Forbidden.
$ ovn-nbctl --columns=cidr,options find DHCP_Options 'external_ids:"neutron:subnet_id"=$SUBNET_ID'
| cidr: 192.168.10.0/24 | options: {server_id=…, router=…, mtu=…, lease_time=…} |
# the switchboard room on compute-7: the room, the line, the operator
$ ip netns | grep ovnmeta-$NETWORK_ID
$ ip netns exec ovnmeta-$NETWORK_ID ss -lnt → LISTEN 169.254.169.254:80
$ ps -ef | grep haproxy | grep $NETWORK_ID
# from inside the VM: the welcome pack itself
$ curl http://169.254.169.254/openstack/latest/meta_data.json → {"uuid": …, "name": "web-1", …}
# 403 Forbidden instead? compare metadata_proxy_shared_secret in the Neutron
# metadata agent config and nova.conf: a mismatch fails every signature
When First Breath Fails
Two of the most common day-two complaints in any OpenStack cloud are born right here: my instance got no IP, and my ssh key never arrived. You now know the machinery, so you know the first place to look.
No IP inside the VM
Is enable_dhcp on for the subnet? Does the NB DHCP_Options row exist? Then module 5's chain: did the port ever go ACTIVE?
Got IP, cloud-init blank
Does ovnmeta-<network-id> exist on that host? Is haproxy running inside it? Read the metadata agent's log.
Metadata returns 403
The shared secret differs between the metadata agent and Nova, so every signature fails verification.
Boot itself hung earlier
Not this module's problem. Walk module 5's five-link chain: claim, up flag, event, block, notification.
Four field scenarios. Answer with the machinery, not with folklore.
A VM boots but gets no IP address. A colleague suggests restarting the DHCP agent on that compute host.
Is that the fix?
Why can the metadata service trust a bare HTTP request that carries no password at all?
Two VMs on different networks call 169.254.169.254 at the same moment, on the same host. How do the responses not get mixed up?
Guests on one network need a bigger MTU advertised to them at boot.
Where does that value actually come from?
The Whole Minute, Machinery Named
In module 1 this was seven mysterious steps. Here it is again, with every piece of machinery named. This is the map you keep.
POST /v2.0/ports arrives at Neutron's API and the ML2 plugin opens the ledger.
ml2/plugin.py · create_portPrecommit reserves MAC, IP, the provisioning block and revision 0 in one transaction; postcommit publishes the Logical_Switch_Port blueprint.
mech_driver + ovn_client → NB DBNova proposes a host; the OVN driver confirms that host's chassis is alive and suitable, then binding is committed.
bind_port · SB ChassisNova's os-vif plugs the TAP device into Open vSwitch with iface-id set to the port ID, then pauses the boot and waits.
os-vif · br-int · iface-idIt claims the port in SB Port_Binding, programs the flows, and flips the logical port to up=true.
ovn-controller · Port_Bindingneutron-server's OVSDB monitor hears up=true, completes the provisioning block, flips the port ACTIVE and fires network-vif-plugged at Nova.
ovsdb_monitor · provisioning_blocks · notifiers/novaLocal flows answer DHCP from DHCP_Options data; the ovnmeta namespace, haproxy and metadata agent carry the signed call to Nova; cloud-init applies the welcome pack.
DHCP_Options · agent/ovn/metadataWhere to Explore Next in the Code
Everything in this course came from real files in the Neutron tree. These four places are where the story lives; each one rewards a slow read.
That was the invisible half of your minute. Next time an instance sticks in BUILD, boots with no IP, or arrives without your key, you will not be guessing: you can name the link that broke and point a teammate, or an AI assistant, straight at it.
end of course · Plugged In: how Neutron wires a VM with OVN