🚀 Your First OpenStack Instance: Key Pair, Security Group, Floating IP and the Mistakes Everybody Makes

Launch a virtual machine on your test cloud and actually log in to it: the image, flavor, network, key pair, security group and floating IP, what the guest sees from inside, what stop, snapshot and resize really do, and every error message you will meet, all run for real

💡 How this page was made. Everything shown as output was run for real on 8 October 2026 on the DevStack cloud from the previous page (stable/2026.1, one machine with 4 CPUs and 8 GB of RAM). Commands are run as the ordinary demo user (the devstack entry in clouds.yaml) unless a line says otherwise. The guest is CirrOS 0.6.3, a tiny test Linux (login user cirros). In the examples, the ssh commands are shortened: the lab also passed -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes and the key with -i. The addresses and IDs are from this run and will differ on your cloud. Horizon (the web dashboard) was not used: everything is the command line, which is also what you will automate later.

The shopping list

To start a virtual machine (OpenStack says server or instance) you must answer six questions. Each one is a separate object in the cloud, and a failed launch is nearly always one of them:

First, look at what the cloud offers an ordinary user. Nothing has been launched yet:

$ openstack image list
+--------------------------------------+--------------------------+--------+
| ID                                   | Name                     | Status |
+--------------------------------------+--------------------------+--------+
| 0a909d81-1440-4430-a266-930399e6e8b5 | cirros-0.6.3-x86_64-disk | active |
+--------------------------------------+--------------------------+--------+
$ openstack flavor list -c Name -c RAM -c Disk -c VCPUs
+-----------+-------+------+-------+
| Name      |   RAM | Disk | VCPUs |
+-----------+-------+------+-------+
| m1.tiny   |   512 |    1 |     1 |
| m1.small  |  2048 |   20 |     1 |
| m1.medium |  4096 |   40 |     2 |
| m1.large  |  8192 |   80 |     4 |
| m1.xlarge | 16384 |  160 |     8 |
| cirros256 |   256 |    1 |     1 |
| ds512M    |   512 |    5 |     1 |
| ds1G      |  1024 |   10 |     1 |
| ds2G      |  2048 |   10 |     2 |
| ds4G      |  4096 |   20 |     4 |
+-----------+-------+------+-------+
$ openstack network list -c Name
+---------+
| Name    |
+---------+
| private |
| public  |
+---------+
$ openstack subnet list -c Name -c Subnet
+---------------------+---------------------+
| Name                | Subnet              |
+---------------------+---------------------+
| ipv6-public-subnet  | 2001:db8::/64       |
| ipv6-private-subnet | fd28:9fa8:c0f3::/64 |
| public-subnet       | 172.24.4.0/24       |
| private-subnet      | 10.0.0.0/26         |
+---------------------+---------------------+
$ openstack router show router1 -c external_gateway_info -f yaml       (trimmed)
external_gateway_info:
  enable_snat: true
  external_fixed_ips:
  - ip_address: 172.24.4.42
    subnet_id: 5779a8f6-7e49-44f2-825b-c7ef21f0cfea
  - ip_address: 2001:db8::289
    subnet_id: 0900f602-1ded-41f9-98cf-7c20125f3389
  network_id: 666227c5-dc27-4729-80d4-3c4a57a30671
$ openstack security group rule list default -c Direction -c Ethertype -c "IP Protocol" -c "Remote Security Group"
+-------------+-----------+-----------+--------------------------------------+
| IP Protocol | Ethertype | Direction | Remote Security Group                |
+-------------+-----------+-----------+--------------------------------------+
| None        | IPv4      | egress    | None                                 |
| None        | IPv4      | ingress   | 9dfbccd3-8794-4c24-bf11-663d0ca7cfda |
| None        | IPv6      | ingress   | 9dfbccd3-8794-4c24-bf11-663d0ca7cfda |
| None        | IPv6      | egress    | None                                 |
+-------------+-----------+-----------+--------------------------------------+
$ openstack keypair list
$ openstack floating ip list
$ openstack limits show --absolute -c Name -c Value | head -10
+-----------------------------+-------+
| Name                        | Value |
+-----------------------------+-------+
| floating_ips                |    -1 |
| floating_ips_used           |     0 |
| image_meta                  |   128 |
| instances                   |    10 |
| instances_used              |     0 |
| keypairs                    |   100 |
| max_image_meta              |   128 |

Reading it:

Step 1: a key pair

Instances are normally logged into with an SSH key, not a password. The cloud keeps the public half of your key and puts it into each instance when it boots. Make a key on your own machine and import the public half:

$ ssh-keygen -t ed25519 -N "" -C demo@cloud1 -f ~/.ssh/lab_ed25519
Generating public/private ed25519 key pair.
Your identification has been saved in /home/stack/.ssh/lab_ed25519
Your public key has been saved in /home/stack/.ssh/lab_ed25519.pub
$ openstack keypair create --public-key ~/.ssh/lab_ed25519.pub mykey
+-------------+-------------------------------------------------+
| Field       | Value                                           |
+-------------+-------------------------------------------------+
| created_at  | None                                            |
| fingerprint | 19:7f:93:5d:2d:ff:cb:5b:d3:11:b4:1e:59:61:d8:16 |
| id          | mykey                                           |
| is_deleted  | None                                            |
| name        | mykey                                           |
| type        | ssh                                             |
| user_id     | 9afa0116f79045e6bf150ed7b8924336                |
+-------------+-------------------------------------------------+
$ openstack keypair list
+-------+-------------------------------------------------+------+
| Name  | Fingerprint                                     | Type |
+-------+-------------------------------------------------+------+
| mykey | 19:7f:93:5d:2d:ff:cb:5b:d3:11:b4:1e:59:61:d8:16 | ssh  |
+-------+-------------------------------------------------+------+
$ ssh-keygen -lf ~/.ssh/lab_ed25519.pub
256 SHA256:IZP2X2InX9m6WNSzSFom6vF+nMG+Qm1rGP5Vdp2PUog demo@cloud1 (ED25519)

Three things to notice:

Step 2: boot it

The whole launch is one command. Read it before running it: every option is one of the six answers.

openstack server create \
    --flavor cirros256 \
    --image cirros-0.6.3-x86_64-disk \
    --network private \
    --key-name mykey \
    --wait \
    first

The last word is the name of the server. --wait makes the command wait until the server is built, instead of returning at once. This is what it printed, and how long it took:

$ time openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private --key-name mykey --wait first      (the flavor row is left out)
+-----------------+---------------------------------------------------------+
| Field           | Value                                                   |
+-----------------+---------------------------------------------------------+
| addresses       | private=10.0.0.21, fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1 |
| id              | b71604aa-128c-404a-a6d9-2cbea7a6e016                    |
| key_name        | mykey                                                   |
| name            | first                                                   |
| security_groups | name='default'                                          |
| status          | ACTIVE                                                  |
+-----------------+---------------------------------------------------------+

real	0m19.145s
$ openstack server list -c Name -c Status -c Networks
+-------+--------+---------------------------------------------------------+
| Name  | Status | Networks                                                |
+-------+--------+---------------------------------------------------------+
| first | ACTIVE | private=10.0.0.21, fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1 |
+-------+--------+---------------------------------------------------------+
$ openstack server show first -c status -c image -c key_name -c security_groups -c addresses -c OS-EXT-AZ:availability_zone -c created -f yaml
OS-EXT-AZ:availability_zone: nova
addresses:
  private:
  - fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1
  - 10.0.0.21
created: '2026-10-08T23:11:05Z'
image: cirros-0.6.3-x86_64-disk (0a909d81-1440-4430-a266-930399e6e8b5)
key_name: mykey
security_groups:
- name: default
status: ACTIVE

19 seconds from the command to ACTIVE. The server got two addresses on the private network, an IPv4 one (10.0.0.21) and an IPv6 one, from the pools on the private network, and it is in the default security group because you named none. It is also in the availability zone nova and has the image, flavor and key it was started with.

The states you will see while waiting: BUILD (being created), then ACTIVE (running), and ERROR if the build failed. Without --wait, run openstack server list and watch the status.

What the guest says at boot

The instance has no screen, but the cloud keeps a console log, the text the guest printed while booting. It is the first place to look when an instance is up but does not behave:

$ openstack console log show first | sed -n '/Starting network/,/=== datasource/p'        (trimmed)
Starting network: dhcpcd-9.4.1 starting
eth0: waiting for carrier
eth0: carrier acquired
eth0: soliciting an IPv6 router
eth0: soliciting a DHCP lease
eth0: offered 10.0.0.21 from 10.0.0.1
eth0: Router Advertisement from fe80::f816:3eff:feaa:4ccd
eth0: adding address fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1/64
eth0: leased 10.0.0.21 for 43200 seconds
eth0: adding route to 10.0.0.0/26
eth0: adding host route to 169.254.169.254 via 10.0.0.2
eth0: adding default route via 10.0.0.1
forked to background, child pid 252
OK
checking http://169.254.169.254/2009-04-04/instance-id
successful after 1/20 tries: up 8.85. iid=i-00000002
failed to get http://169.254.169.254/2009-04-04/user-data
warning: no ec2 metadata for user-data
Starting dropbear sshd: OK
GROWROOT: CHANGED: partition=1 start=18432 old: size=210911 end=229343 new: size=2078687,end=2097119
/dev/root resized successfully [took 0.04s]
=== system information ===
Platform: OpenStack Foundation OpenStack Nova
Arch: x86_64
CPU(s): 1 @ na MHz
RAM Size: 202MB
=== sshd host keys ===
-----BEGIN SSH HOST KEY KEYS-----
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcttx...   (cut)
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGGO...   (cut)
-----END SSH HOST KEY KEYS-----
=== datasource: ec2 net ===
instance-id: i-00000002
availability-zone: nova
local-hostname: first.novalocal
=== pinging gateway failed, debugging connection ===
$ openstack console url show first -c url -f value
http://10.0.2.15:6080/vnc_lite.html?path=%3Ftoken%3Dbec7a256-6332-447d-8875-a2e4cf5a7ba0

The story of a boot, read as the guest tells it:

  1. Network by DHCP. The guest asks for an address and is offered 10.0.0.21 from 10.0.0.1. It also gets an IPv6 address from a router advertisement.
  2. A special route to 169.254.169.254, via 10.0.0.2. That strange address is the metadata service, one of the most important ideas in a cloud: see below.
  3. successful after 1/20 tries: up 8.85. iid=i-00000002: the guest asked the metadata service who it is, and learned its instance ID, 8.85 seconds into its life.
  4. no ec2 metadata for user-data: it also asked for user data (a script to run) and there was none. This is normal when you did not pass any, not an error.
  5. GROWROOT: the image's disk is small, the flavor's disk is bigger, so the guest grew its root partition to fill the flavor (size=210911 sectors became size=2078687).
  6. Host keys: the guest generated its own SSH host keys and printed them, so you can check them out of band before trusting the first connection.

You can also open the console in a browser. openstack console url show prints a time-limited noVNC URL (http://10.0.2.15:6080/vnc_lite.html?path=%3Ftoken%3D...), the equivalent of looking at a screen when the network is broken. In this lab the port was not forwarded to the laptop, so it was not opened.

One more honest line from the log: near its end, CirrOS printed pinging gateway failed, debugging connection. It does a quick test of its gateway at boot, and on this boot that test failed, yet the instance's network worked straight afterwards (it answered ping and ssh within a minute, and pinged its gateway from inside). I did not find out why. If you see the message but can log in, it is not the problem.

Step 3: why you cannot reach it yet

The instance has a private address, 10.0.0.21, on a network that only exists inside the cloud. To reach it from outside you need a floating IP: an address from the public network that the router maps one-to-one onto the instance's private address (NAT in both directions).

$ openstack floating ip create public -c floating_ip_address -c floating_network_id -c status -c port_id
+---------------------+--------------------------------------+
| Field               | Value                                |
+---------------------+--------------------------------------+
| floating_ip_address | 172.24.4.68                          |
| floating_network_id | 666227c5-dc27-4729-80d4-3c4a57a30671 |
| port_id             | None                                 |
| status              | DOWN                                 |
+---------------------+--------------------------------------+
$ openstack server add floating ip first 172.24.4.68
$ openstack floating ip list -c "Floating IP Address" -c "Fixed IP Address" -c Status
+---------------------+------------------+
| Floating IP Address | Fixed IP Address |
+---------------------+------------------+
| 172.24.4.68         | 10.0.0.21        |
+---------------------+------------------+
$ openstack server list -c Name -c Networks
+-------+----------------------------------------------------------------------+
| Name  | Networks                                                             |
+-------+----------------------------------------------------------------------+
| first | private=10.0.0.21, 172.24.4.68, fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1 |
+-------+----------------------------------------------------------------------+

Allocating a floating IP (floating ip create public) only reserves one for your project (port_id: None, status DOWN). Attaching it (server add floating ip) is a separate step, which is why it can be moved between servers later. Afterwards the server list shows three addresses: the private IPv4 one, the floating one, and the IPv6 one.

The instance itself does not know about its floating IP: inside, its network has only 10.0.0.21 (the metadata service is what tells it, see below). The NAT happens in the router.

Step 4: the security group, and the bouncer

The server is ACTIVE, it has a floating IP and a key, and:

$ ping -c3 -W2 172.24.4.68
PING 172.24.4.68 (172.24.4.68) 56(84) bytes of data.

--- 172.24.4.68 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2056ms

$ ssh -i ~/.ssh/lab_ed25519 cirros@172.24.4.68 hostname
ssh: connect to host 172.24.4.68 port 22: Connection timed out

100% packet loss on ping, and an SSH timeout. Nothing is wrong with the instance, the network or the key. The default security group allows incoming traffic only from other members of the same group, and your laptop is not one. Everything else is dropped, silently, which is why the symptom is a timeout and not "connection refused". This is the single most common first-day OpenStack problem, and the same thing happens on every other cloud.

Security groups are allow lists attached to an instance's network port. There are no deny rules: a packet is allowed if any rule of any group on the port allows it. They are stateful (stateful: True in the output below): when a connection is allowed in, the replies are allowed out without another rule. So you do not edit the default group; you make a group for the job and add it. Here, one that allows ping and SSH:

$ openstack security group create ssh-ping --description "ssh and ping from anywhere"       (the table is trimmed)
| id              | 4c52887d-6277-4473-b0f6-12f0796e62d5 |
| name            | ssh-ping                             |
| stateful        | True                                 |
$ openstack security group rule create --proto icmp --ingress ssh-ping -c direction -c protocol -c remote_ip_prefix -f value | tr "\n" " "
ingress icmp 0.0.0.0/0
$ openstack security group rule create --proto tcp --dst-port 22 --ingress ssh-ping -c direction -c port_range_min -c port_range_max -c protocol -c remote_ip_prefix -f value | tr "\n" " "
ingress 22 22 tcp 0.0.0.0/0
$ openstack security group rule list ssh-ping -c Direction -c Ethertype -c "IP Protocol" -c "Port Range" -c "IP Range"
+-------------+-----------+-----------+------------+-----------+
| IP Protocol | Ethertype | IP Range  | Port Range | Direction |
+-------------+-----------+-----------+------------+-----------+
| None        | IPv4      | 0.0.0.0/0 |            | egress    |
| None        | IPv6      | ::/0      |            | egress    |
| icmp        | IPv4      | 0.0.0.0/0 |            | ingress   |
| tcp         | IPv4      | 0.0.0.0/0 | 22:22      | ingress   |
+-------------+-----------+-----------+------------+-----------+
$ openstack server add security group first ssh-ping
$ openstack server show first -c security_groups -f value
[{'name': 'ssh-ping'}, {'name': 'default'}]
$ ping -c3 -W2 172.24.4.68 | tail -4

--- 172.24.4.68 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.196/1.813/2.661/0.619 ms
$ ssh -i ~/.ssh/lab_ed25519 cirros@172.24.4.68 'echo "hello from $(hostname)"; uname -sr; id'
hello from first
Linux 5.15.0-117-generic
uid=1000(cirros) gid=1000(cirros) groups=1000(cirros)

What the rules mean:

Ping works, and SSH works with the key pair from step 1: hello from first, a CirrOS kernel and the cirros user. This is the whole point of the page: a server you can log in to, built in four commands and a security group.

From the inside

CirrOS is tiny, but it shows what the cloud gave the instance:

$ (inside) hostname; cat /etc/os-release | head -2; uptime
first
PRETTY_NAME="CirrOS 0.6.3"
NAME="CirrOS"
 00:13:34 up 2 min,  0 users,  load average: 0.00, 0.00, 0.00
$ (inside) ip route
default via 10.0.0.1 dev eth0  src 10.0.0.21  metric 1002
10.0.0.0/26 dev eth0 scope link  src 10.0.0.21  metric 1002
169.254.169.254 via 10.0.0.2 dev eth0  src 10.0.0.21  metric 1002
$ (inside) cat /home/cirros/.ssh/authorized_keys | cut -c1-80
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOvWkfC5wL/CELzlv5riQqkAlgZ92vzp6F9yx0MU6yi
$ (inside) df -h / | tail -1; free -m | sed -n 2p
/dev/vda1               960.8M     43.9M    885.2M   5% /
Mem:            203          31         127           4          44         160
$ (inside) ping -c1 -W2 10.0.0.1 | tail -2
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.991/1.991/1.991/0.000 ms
$ (inside) ping -c1 -W2 172.24.4.1 | tail -2
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.179/3.179/3.179/0.000 ms
$ (inside) ping -c1 -W2 1.1.1.1 | tail -2
1 packets transmitted, 0 received, 100% packet loss, time 0ms
$ (inside) cat /etc/resolv.conf
# Generated by dhcpcd from eth0.dhcp
# /etc/resolv.conf.head can replace this line
nameserver 127.0.0.53
# /etc/resolv.conf.tail can replace this line
$ (inside) nslookup example.com
nslookup: write to '127.0.0.53': Connection refused
;; connection timed out; no servers could be reached

The metadata service

The address 169.254.169.254 is answered by the cloud, not by a real machine. Every instance can ask it questions over plain HTTP, and what it learns comes from the cloud's own database:

$ (inside) wget -qO- http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
instance-action
instance-id
instance-type
local-hostname
local-ipv4
placement/
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
$ (inside) wget -qO- http://169.254.169.254/latest/meta-data/instance-id; wget -qO- .../hostname; wget -qO- .../local-ipv4; wget -qO- .../public-ipv4
i-00000002
first.novalocal
10.0.0.21
172.24.4.68
$ (inside) wget -qO- http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key | cut -c1-70
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOvWkfC5wL/CELzlv5riQqkAlgZ92vzp6
$ (inside) wget -qO- http://169.254.169.254/openstack/latest/meta_data.json | head -c 420
{"uuid": "b71604aa-128c-404a-a6d9-2cbea7a6e016", "public_keys": {"mykey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOvWkfC5wL/CELzlv5riQqkAlgZ92vzp6F9yx0MU6yi demo@cloud1\n"}, "keys": [{"name": "mykey", "type": "ssh", "data": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOvWkfC5wL/CELzlv5riQqkAlgZ92vzp6F9yx0MU6yi demo@cloud1\n"}], "hostname": "first.novalocal", "name": "first", "launch_index": 0, "availability_zone": "nova", "random_seed": "eOO3k5XsVhtLEL6dENrl3WR...

User data is a script (or a configuration) you give at boot with --user-data FILE. Normal images run it once, on the first boot (the repeat behaviour was not tested here). This one, a small shell script, ran on a second server:

$ cat hello-data.sh
#!/bin/sh
echo "booted at $(date -u +%H:%M:%S) by user-data" > /tmp/hello.txt
echo "my name is $(hostname)" >> /tmp/hello.txt
$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private --key-name mykey --security-group ssh-ping --user-data hello-data.sh --wait second -c status -c addresses -f yaml
addresses:
  private:
  - fd28:9fa8:c0f3:0:f816:3eff:fe94:3177
  - 10.0.0.4
status: ACTIVE
$ openstack server add floating ip second 172.24.4.217
$ ssh -i ~/.ssh/lab_ed25519 cirros@172.24.4.217 'cat /tmp/hello.txt'
booted at 23:16:55 by user-data
my name is second

/tmp/hello.txt was written during boot, before anyone logged in. On a normal Linux image the same mechanism installs packages, creates users and starts services: see the cloud-init page. Two rules from the console log earlier: user data is only run if the guest asks for it (no user data means that harmless warning), and it is not secret: any process in the instance can read it, so never put passwords in it.

Reaching the outside world (a DevStack detail)

The failures above are not about OpenStack in general. They are about this test cloud sitting inside one virtual machine. Two things were missing, and both are worth knowing because the symptoms are the same on a real cloud when the network is half-built:

  1. The instance had no DNS server. The private subnet's list of DNS servers was empty, so the instance was handed the DevStack machine's own 127.0.0.53. The fix is a DNS setting on the subnet, which the cloud's DHCP then hands to every instance.
  2. Nothing translated the cloud's outside addresses. The router sends instance traffic out of the public network with its address 172.24.4.42, to the DevStack machine, which has to forward it to the real network, and its upstream does not know about 172.24.4.0/24. One firewall rule (NAT) on the DevStack machine fixes that.
$ openstack subnet show private-subnet -c dns_nameservers -c gateway_ip -c cidr -c enable_dhcp -f yaml
cidr: 10.0.0.0/26
dns_nameservers: []
enable_dhcp: true
gateway_ip: 10.0.0.1
$ openstack subnet set --dns-nameserver 1.1.1.1 --dns-nameserver 9.9.9.9 private-subnet
$ openstack subnet show private-subnet -c dns_nameservers -f value
['1.1.1.1', '9.9.9.9']
$ sudo iptables -t nat -A POSTROUTING -s 172.24.4.0/24 ! -d 172.24.4.0/24 -j MASQUERADE        (on the DevStack machine)
$ sudo iptables -t nat -S POSTROUTING | tail -3
-P POSTROUTING ACCEPT
-A POSTROUTING -j LIBVIRT_PRT
-A POSTROUTING -s 172.24.4.0/24 ! -d 172.24.4.0/24 -j MASQUERADE
$ openstack server reboot first        (so that the guest asks DHCP again)
$ (inside) grep nameserver /etc/resolv.conf
nameserver 1.1.1.1
nameserver 9.9.9.9
$ (inside) nslookup example.com | head -6
Server:		1.1.1.1
Address:	1.1.1.1:53

Non-authoritative answer:
Name:	example.com
Address: 172.66.147.243
$ (inside) wget -qO- -T 8 http://example.com | head -c 60
<!doctype html><html lang=en><head><meta charset=utf-8><link
$ sudo iptables -t nat -D POSTROUTING -s 172.24.4.0/24 ! -d 172.24.4.0/24 -j MASQUERADE      (remove it again, as an experiment)
$ (inside) wget -qO- -T 6 http://example.com
wget: bad address 'example.com'
$ sudo iptables -t nat -A POSTROUTING -s 172.24.4.0/24 ! -d 172.24.4.0/24 -j MASQUERADE      (and put it back)
$ (inside) wget -qO- -T 8 http://example.com | head -c 60
<!doctype html><html lang=en><head><meta charset=utf-8><link

After that, a DNS lookup and an HTTP fetch from inside the instance work. The experiment in the middle proves the NAT rule is the part that matters: remove it and the name lookup fails again (wget: bad address), put it back and it works. The instance was rebooted so that it would ask DHCP again and pick up the new DNS servers.

Two notes. ping 1.1.1.1 still fails from inside the instance, but it fails from the DevStack machine itself too: the virtual network of this particular lab does not pass ICMP to the internet, so use wget or nslookup to test. And the NAT rule is a plain iptables command that is not saved anywhere (the lab did not reboot to test it, but nothing writes it down, like the br-ex address on the previous page): a real cloud's provider network routes or NATs for you.

Stop, start, reboot, snapshot, resize, delete

Stop and start. openstack server stop powers the instance off, start turns it back on. In between the status is SHUTOFF, the floating IP stays attached (the server list still shows it), and it is of course unreachable (ping got 100% loss). And one lesson that surprised the author: what you wrote and did not sync can be lost.

$ ssh cirros@172.24.4.68 'echo synced > /home/cirros/with-sync; sync; echo not-synced > /home/cirros/no-sync'
$ openstack server stop first
SHUTOFF
$ openstack server show first -c status -c OS-EXT-STS:power_state -c OS-EXT-STS:vm_state -f value | tr "\n" " "
4 stopped SHUTOFF
$ sudo virsh list --all
 Id   Name                State
------------------------------------
 4    instance-00000003   running
 -    instance-00000002   shut off
$ ping -c2 -W2 172.24.4.68 | tail -3
--- 172.24.4.68 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1057ms
$ openstack server list -c Name -c Status -c Networks
+--------+---------+----------------------------------------------------------------------+
| Name   | Status  | Networks                                                             |
+--------+---------+----------------------------------------------------------------------+
| second | ACTIVE  | private=10.0.0.4, 172.24.4.217, fd28:9fa8:c0f3:0:f816:3eff:fe94:3177 |
| first  | SHUTOFF | private=10.0.0.21, 172.24.4.68, fd28:9fa8:c0f3:0:f816:3eff:fe11:54c1 |
+--------+---------+----------------------------------------------------------------------+
$ openstack server start first
ACTIVE
$ ssh cirros@172.24.4.68 'ls -la /home/cirros; cat /home/cirros/with-sync /home/cirros/no-sync /home/cirros/snapnote; uptime'      (the . and .profile lines are left out)
-rw-------    1 cirros   cirros          16 Oct  9 00:18 snapnote
-rw-------    1 cirros   cirros           7 Oct  9 00:19 with-sync
synced
cat: can't open '/home/cirros/no-sync': No such file or directory
in-the-snapshot
 00:20:29 up 0 min,  0 users,  load average: 0.00, 0.00, 0.00
$ journalctl -u devstack@n-cpu | grep "Instance destroyed"
INFO nova.virt.libvirt.driver [-] [instance: b71604aa-128c-404a-a6d9-2cbea7a6e016] Instance destroyed successfully.

Two files were written just before the stop: one followed by sync, one not. The synced file survived; the other did not exist after the start (and an earlier note, written the same way, was lost too). Nova's log says why: Instance destroyed successfully, within a couple of seconds, in other words in this lab the guest did not shut down cleanly and was cut, like pulling the plug. The root disk is an ordinary ext3 filesystem. The lesson holds on any cloud: a stop is only as clean as the guest makes it. Shut down from inside (sudo poweroff) or sync first when the data matters.

Reboot (openstack server reboot, add --hard for a power-cycle) restarts the guest. It takes longer than the API call: SSH was refused 20 seconds after the command, because the guest was still booting. Wait for sshd.

Snapshot. A snapshot turns a running server's disk into a new image that you can boot from:

$ time openstack server image create --name first-snap --wait first -c id -c name -c status -f value
ea63dcb1-ce6d-407a-9dd1-2d0e807cf43b
first-snap
active

real	0m15.323s
$ openstack image list -c Name -c Status
+--------------------------+--------+
| Name                     | Status |
+--------------------------+--------+
| cirros-0.6.3-x86_64-disk | active |
| first-snap               | active |
+--------------------------+--------+
$ openstack image show first-snap -c name -c disk_format -c container_format -c status -c properties -f yaml | head -9
container_format: bare
disk_format: qcow2
name: first-snap
properties:
  base_image_ref: 0a909d81-1440-4430-a266-930399e6e8b5
  boot_roles: anotherrole,reader,member
  hw_cdrom_bus: ide
  hw_disk_bus: virtio
  hw_machine_type: pc
$ openstack server create --flavor cirros256 --image first-snap --network private --key-name mykey --security-group ssh-ping --wait from-snap -c status -f value
ACTIVE
$ ssh cirros@172.24.4.252 'hostname; ls /home/cirros; cat /home/cirros/snapnote; cat /home/cirros/with-sync'
from-snap
snapnote
in-the-snapshot
cat: can't open '/home/cirros/with-sync': No such file or directory
$ openstack server list -c Name -c Status -c Image -c Flavor
+-----------+--------+--------------------------+-----------+
| Name      | Status | Image                    | Flavor    |
+-----------+--------+--------------------------+-----------+
| from-snap | ACTIVE | first-snap               | cirros256 |
| second    | ACTIVE | cirros-0.6.3-x86_64-disk | cirros256 |
| first     | ACTIVE | cirros-0.6.3-x86_64-disk | cirros256 |
+-----------+--------+--------------------------+-----------+

It took 15 seconds and appears next to the original image, active, as qcow2, with properties that remember where it came from (base_image_ref). A new server booted from first-snap has snapnote, written before the snapshot, and does not have with-sync, written after it: a snapshot is a copy of the disk at one moment. It is a good way to keep a configured server, and it is not a backup of your data over time (see the backups page).

Resize changes the flavor:

$ openstack server resize --flavor m1.tiny second
RESIZE RESIZE RESIZE VERIFY_RESIZE
$ openstack server resize confirm second
VERIFY_RESIZE ACTIVE
$ ssh cirros@172.24.4.217 'free -m | sed -n 2p; cat /tmp/hello.txt'
Mem:            454          31         397           4          26         407
booted at 23:16:55 by user-data
my name is second
$ openstack server show second -c flavor -f value | cut -c1-90
{'name': 'm1.tiny', 'original_name': 'm1.tiny', 'description': None, 'disk': 1, 'is_public': True, 'ram': 512, 'vcpus':

The server went through RESIZE to VERIFY_RESIZE, and Nova waits for you to resize confirm (or resize revert) before it forgets the old size. Afterwards the guest saw 454 MB instead of 203 MB, and the file written by the user-data script was still there. On this one-host cloud it worked on the same host; moving a server between hosts is part of what a resize can do on a bigger one (not shown here).

Delete. openstack server delete NAME removes the server and its disk. The rest of the clean-up is your job, and the order matters:

$ openstack server delete --wait first second again nonet
$ openstack server list
$ openstack floating ip list -c "Floating IP Address" -c "Fixed IP Address" -c Status
+---------------------+------------------+
| Floating IP Address | Fixed IP Address |
+---------------------+------------------+
| 172.24.4.252        | None             |
| 172.24.4.68         | None             |
| 172.24.4.217        | None             |
+---------------------+------------------+
$ openstack floating ip delete $(openstack floating ip list -f value -c ID)
$ openstack security group delete ssh-ping
$ openstack security group list -c Name
+---------+
| Name    |
+---------+
| default |
+---------+
$ openstack keypair delete mykey
$ openstack image delete first-snap
$ openstack limits show --absolute -c Name -c Value | grep -E "^\| (instances_used|floating_ips_used|security_groups_used|total_cores_used|total_ram_used) "      (five rows of the list)
| floating_ips_used           |     0 |
| instances_used              |     0 |
| security_groups_used        |     0 |
| total_cores_used            |     0 |
| total_ram_used              |     0 |

When things go wrong

These are the real messages, produced on purpose:

$ openstack server create --flavor cirros256 --image nosuchimage --network private x
No Image found for nosuchimage
$ openstack server create --flavor nosuchflavor --image cirros-0.6.3-x86_64-disk --network private x
No Flavor found for nosuchflavor
$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private --key-name nokey x
BadRequestException: 400: Client Error for url: http://10.0.2.15/compute/v2.1/servers, Invalid key_name provided.
$ openstack keypair create --public-key ~/.ssh/lab_ed25519.pub mykey
ConflictException: 409: Client Error for url: http://10.0.2.15/compute/v2.1/os-keypairs, Key pair 'mykey' already exists.
$ openstack security group rule create --proto tcp --dst-port 22 --ingress ssh-ping
Error while executing command: ConflictException: 409, Security group rule already exists. Rule id is fd551162-4138-4d13-b2cd-ccf61d8d1037.
$ openstack security group delete ssh-ping            (while servers still use it)
Failed to delete group with name or ID 'ssh-ping': ConflictException: 409: Client Error for url: http://10.0.2.15/networking/v2.0/security-groups/4c52887d-6277-4473-b0f6-12f0796e62d5, Security Group 4c52887d-6277-4473-b0f6-12f0796e62d5 in use.
1 of 1 groups failed to delete.
$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk nonet        (no --network at all; the table it prints is left out)
$ openstack server show nonet -c addresses -c security_groups -c key_name -f yaml
addresses:
  private:
  - fd28:9fa8:c0f3:0:f816:3eff:fe30:acdd
  - 10.0.0.3
key_name: null
security_groups:
- name: default

The server was put on the only private network in the project. With more than one private network Nova refuses to guess and asks you to choose with --network (not run here, the lab has only one). In scripts, always name the network.

And the one that confuses everybody, ERROR with No valid host was found:

$ openstack server create --flavor m1.xlarge --image cirros-0.6.3-x86_64-disk --network private toobig2
$ openstack server show 153abf13-9a03-4680-9911-9852d9c27b98 -c status -c fault -f yaml            (as the demo user)
fault:
  code: 500
  created: '2026-10-08T23:23:59Z'
  message: 'No valid host was found. '
status: ERROR
$ OS_CLOUD=devstack-admin openstack server show 153abf13-9a03-4680-9911-9852d9c27b98 -c fault -f yaml | head -5      (as admin: the same, plus a traceback)
fault:
  code: 500
  created: '2026-10-08T23:23:59Z'
  details: "Traceback (most recent call last):\n  File \"/opt/stack/nova/nova/conductor/manager.py\", line 1677, in schedule_and_build_instances\n ...
$ journalctl -u devstack@n-sch | grep -i "no allocation candidates"
INFO nova.scheduler.manager [None req-d3be9af0-3acb-40da-9f8e-bccf465aab67 demo demo] Got no allocation candidates from the Placement API. This could be due to insufficient resources or a temporary occurrence as compute nodes star...
$ openstack flavor show m1.xlarge -c ram -c vcpus -c disk -f value | tr "\n" " "
160 16384 8
$ openstack resource provider inventory list <the host> -c resource_class -c total -c reserved -c used
+----------------+----------+-------+------+
| resource_class | reserved | total | used |
+----------------+----------+-------+------+
| VCPU           |        0 |     4 |    4 |
| MEMORY_MB      |      512 |  7941 | 1280 |
| DISK_GB        |        0 |    57 |    4 |
+----------------+----------+-------+------+

The demo user sees only the one-line message and an error code 500. The admin sees a traceback, and the scheduler's log gives the actual reason: "Got no allocation candidates from the Placement API. This could be due to insufficient resources". The flavor asked for 16384 MB of RAM and the whole machine has 7941 MB (with 1280 already used): no host could ever fit it. This is what the Placement table on the first page is for. Typical causes of "no valid host": the flavor is too big (RAM, CPUs or disk), the host is full, a compute service is down, or a placement rule excludes every host. Check openstack compute service list and the resource inventory, then read n-sch's log.

Quotas. Each project has limits. A fifth instance on a project limited to four:

$ openstack limits show --absolute -c Name -c Value | grep -E "^\| (instances|instances_used) "
| instances                   |    10 |
| instances_used              |     4 |
$ OS_CLOUD=devstack-admin openstack quota set --instances 4 demo
$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private fifth
ForbiddenException: 403: Client Error for url: http://10.0.2.15/compute/v2.1/servers, Quota exceeded for instances: Requested 1, but already used 4 of 4 instances
$ OS_CLOUD=devstack-admin openstack quota set --instances 10 demo

403 Quota exceeded for instances: Requested 1, but already used 4 of 4 instances says exactly what to do: free something up or ask an admin to raise the quota (quota set, shown as devstack-admin).

SSH says the host key changed. Floating IPs are reused. Delete a server and the next one that gets the same address has a different host key, and ssh refuses to connect, because from its point of view somebody is impersonating the machine you trusted:

$ ssh -o UserKnownHostsFile=kh-test -o StrictHostKeyChecking=accept-new cirros@172.24.4.252 hostname
Warning: Permanently added '172.24.4.252' (ED25519) to the list of known hosts.
from-snap
$ openstack server delete --wait from-snap
$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private --key-name mykey --security-group ssh-ping --wait again
$ openstack server add floating ip again 172.24.4.252
$ ssh -o UserKnownHostsFile=kh-test -o StrictHostKeyChecking=accept-new cirros@172.24.4.252 hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:itSIriUWh/fYB+UoeAfexQN8Xshw9MnPkBph64YdzNE.
Please contact your system administrator.
$ ssh-keygen -R 172.24.4.252 -f kh-test
# Host 172.24.4.252 found: line 1
kh-test updated.
Original contents retained as kh-test.old
$ ssh -o UserKnownHostsFile=kh-test -o StrictHostKeyChecking=accept-new cirros@172.24.4.252 hostname
Warning: Permanently added '172.24.4.252' (ED25519) to the list of known hosts.
again

It is a false alarm here: you know the old server was deleted. ssh-keygen -R ADDRESS forgets the old key, and the next connection records the new one. Do that only when you know the machine really was rebuilt. (The lab used a separate known_hosts file for the test, to leave the real one alone.)

The pitfalls, in one list

What this page did not cover

The cheat sheet

openstack image list / flavor list / network list         what you can choose from
ssh-keygen -t ed25519 -f ~/.ssh/KEY                       make a key (on your machine)
openstack keypair create --public-key ~/.ssh/KEY.pub NAME      import the public half
openstack security group create NAME                      a new group (egress only)
openstack security group rule create --proto tcp --dst-port 22 --ingress NAME
openstack security group rule create --proto icmp --ingress NAME
openstack server create --flavor F --image I --network N --key-name K --security-group G --wait NAME
openstack server list / show NAME                         status, addresses, groups
openstack console log show NAME                           what the guest printed at boot
openstack console url show NAME                           browser console (noVNC)
openstack floating ip create public                       reserve an outside address
openstack server add floating ip NAME ADDRESS             attach it
openstack server add security group NAME GROUP            change the rules on a running server
openstack server stop / start / reboot [--hard] NAME
openstack server image create --name SNAP --wait NAME     snapshot to a new image
openstack server resize --flavor F NAME  then  resize confirm NAME
openstack subnet set --dns-nameserver 1.1.1.1 SUBNET      DNS for every instance on it
openstack server delete --wait NAME
openstack floating ip delete ADDRESS-ID                   they do NOT go away with the server
openstack limits show --absolute                          quotas and what you are using
ssh-keygen -R ADDRESS                                     forget a reused address's old host key

← Build a Test Cloud with DevStack: From a Blank Ubuntu to a Working Cloud · ↑ All tutorials · Persistent Volumes on OpenStack: Cinder, Snapshots, Resize and Boot from Volume →