Skip to content

OpenStack single-stack IPv6 support #1909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

yasminvalim
Copy link
Contributor

@yasminvalim yasminvalim commented Jul 15, 2024

In #1897 issue, we are requested to create support for IPv6 so Ignition can fetch the metadata in single-stack environments.

To achieve this, we added a new URL with the IPv6 endpoint. We also created logic to first attempt the IPv4 endpoint so If this fails, it will try the IPv6 one. If both endpoints fail, it will return the appropriate error.

@yasminvalim yasminvalim self-assigned this Jul 15, 2024
@yasminvalim yasminvalim marked this pull request as draft July 15, 2024 21:01
@yasminvalim yasminvalim force-pushed the openstack-support branch 2 times, most recently from 9e7cba8 to 87e4a48 Compare July 16, 2024 18:30
@travier
Copy link
Member

travier commented Jul 17, 2024

Not sure that will work. What happens if the instance is IPv6 only? Do requests fail differently than if the network is not up yet?

@travier
Copy link
Member

travier commented Jul 17, 2024

One of the principle behind Ignition is that it will retry indefinitely until the server explicitly returns an error.

@yasminvalim
Copy link
Contributor Author

Not sure that will work. What happens if the instance is IPv6 only? Do requests fail differently than if the network is not up yet?

I updated my code logic. In an IPv6-only instance, IPv4 will fail because it's not found and the resources will be returned from IPv6. If both fails it will return errors. The error will be handled with a message in dispatch function above. It makes sense?

One thing I need to consider is the possibility of having both IPv4 and IPv6 simultaneously. Is that possible? If yes, I need to handle this case as well.

@yasminvalim
Copy link
Contributor Author

One of the principle behind Ignition is that it will retry indefinitely until the server explicitly returns an error.

Nice. I guess this is being handled in func fetchConfig, right? There is a loop in there like this:

Loop:
	for {
		select {
		case <-ctx.Done():
			break Loop
		case <-errChan:
			dispatchCount--
			if dispatchCount == 0 {
				f.Logger.Info("couldn't fetch config")
				break Loop
			}
		}
	}

	return util.ParseConfig(f.Logger, data)
}

Thanks!

@travier
Copy link
Member

travier commented Jul 22, 2024

One thing I need to consider is the possibility of having both IPv4 and IPv6 simultaneously. Is that possible? If yes, I need to handle this case as well.

Yes, this is very common.

@yasminvalim
Copy link
Contributor Author

One thing I need to consider is the possibility of having both IPv4 and IPv6 simultaneously. Is that possible? If yes, I need to handle this case as well.

Yes, this is very common.

I updated the code to cover this scenarios. So, now the code looks like this:

  • If both endpoints have valid data, the results are combined and returned
    I used append, not sure if is the best approach.

  • If only one endpoint has valid data, that data is returned

  • If both endpoints fail, the appropriate error is returned with logs

  • If both endpoints return resource.ErrNotFound, the function returns nil

These should be the basic scenarios. Now, I'm working on running my code in the OpenStack environment and reading the necessary docs to test it correctly.

Thanks for reviewing and helping me with this task!

Copy link

@MaysaMacedo MaysaMacedo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to take into consideration:

Scheme: "http",
Host: "169.254.169.254",
Path: "openstack/latest/user_data",
}
ipv6MetadataServiceUrl = url.URL{
Scheme: "http",
Host: "[fe80::a9fe:a9fe]",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we might need to specify the interface name like it's mentioned on the docs
https://docs.openstack.org/nova/latest/user/metadata.html#the-metadata-service
and how it's done elsewhere
https://github.com/canonical/cloud-init/blob/main/cloudinit/sources/DataSourceOpenStack.py#L76

@yasminvalim yasminvalim force-pushed the openstack-support branch 2 times, most recently from d81e5fb to 6b8a11e Compare August 8, 2024 22:23
}
ipv6MetadataServiceUrl = url.URL{
Scheme: "http",
Host: fmt.Sprintf("[fe80::a9fe:a9fe%%%s]", url.PathEscape(iface)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to change to [fe80::a9fe:a9fe%25%s] refer to https://docs.openstack.org/nova/latest/user/metadata.html#the-metadata-service

@yasminvalim yasminvalim force-pushed the openstack-support branch 3 times, most recently from f2979b8 to fa50682 Compare October 16, 2024 22:35
@yasminvalim yasminvalim force-pushed the openstack-support branch 5 times, most recently from e105dea to 1569e4c Compare January 6, 2025 13:46
Scheme: "http",
Host: "169.254.169.254",
Path: "openstack/latest/user_data",
}
metadataServiceUrlIPv6 = url.URL{
Scheme: "http",
Host: "fe80::a9fe:a9fe%",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried having the host be [fe90::a9fe:a9fe%]? since you are getting an error on fetch due to improper escaping ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @prestist . Looks like you're missing to enclose the host in square brackets.
At least it's documented here.
Maybe you will also need to add the interface name to the host once you have it, before calling FetchToBuffer

@yasminvalim
Copy link
Contributor Author

Hey team, I'm experiencing an error while trying to connect the server. I added some log to the code, then I ran openstack console log show <server-id> command and received the following:

[   18.648461] NetworkManager[808]: <info>  [1738700037.3670] dhcp6 (enp3s0): state changed new lease, address=fd2e:6f44:5dd8:c957::1bd
[   24.819235] ignition[918]: GET result: OK
[   24.826066] ignition[918]: failed to fetch config from metadata service: IPv4 succeeded, but IPv6 failed: parse "http://fe80::a9fe:a9fe%25/openstack/latest/user_data": invalid port ":a9fe%25" after host
[   24.852271] ignition[918]: Successfully fetched configuration from IPv4.
[   24.862505] ignition[918]: Fetching IPv6 address for metadata service...
[   24.872374] ignition[918]: Fetching zone id...
[   24.879246] ignition[918]: Checking interface: lo
[   24.886322] ignition[918]: Checking interface: enp3s0
[   24.893800] ignition[918]: Verificando se o IP �� IPv6: 10.1.0.179, resultado: false
[   24.904868] ignition[918]: Verificando se o IP �� IPv6: fd2e:6f44:5dd8:c957::1bd, resultado: true
[   24.917265] ignition[918]: Active interface found: enp3s0
[   24.925264] ignition[918]: Fetching from IPv6 metadata service at http://[fe80::a9fe:a9fe%%25enp3s0]/openstack/latest/user_data...
[   24.941718] ignition[918]: Fetching from IPv6 metadata service at http://fe80::a9fe:a9fe%25/openstack/latest/user_data...
[   24.960088] ignition[918]: IPv6 metadata service failed: parse "http://fe80::a9fe:a9fe%25/openstack/latest/user_data": invalid port ":a9fe%25" after host

Not sure why is not parsing correctly.

@yasminvalim
Copy link
Contributor Author

yasminvalim commented Feb 6, 2025

[SOLVED] Mobbing session output:

We utilized an OpenStack environment to fetch and curl the IPv6, but were unable to get a result. Here are the logs:

[cloud-user@jcapitao-cs9-metadata-test ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
	link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
	inet 127.0.0.1/8 scope host lo
   	valid_lft forever preferred_lft forever
	inet6 ::1/128 scope host
   	valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
	link/ether fa:16:3e:1f:f8:90 brd ff:ff:ff:ff:ff:ff
	altname enp0s3
	altname ens3
	inet 10.0.79.79/23 brd 10.0.79.255 scope global dynamic noprefixroute eth0
   	valid_lft 41025sec preferred_lft 41025sec
	inet6 2620:52:0:4e:f816:3eff:fe1f:f890/64 scope global dynamic noprefixroute
   	valid_lft 2591943sec preferred_lft 604743sec
	inet6 fe80::f816:3eff:fe1f:f890/64 scope link noprefixroute
   	valid_lft forever preferred_lft forever
[cloud-user@jcapitao-cs9-metadata-test ~]$ curl http://169.254.169.254/openstack/
2012-08-10
2013-04-04
2013-10-17
2015-10-15
2016-06-30
2016-10-06
2017-02-22
2018-08-27
2020-10-14
latest[cloud-user@jcapitao-cs9-metadata-test ~]$
[cloud-user@jcapitao-cs9-metadata-test ~]$ curl -v -g -6 'http://[fe80::a9fe:a9fe%25eth0]'
*   Trying fe80::a9fe:a9fe:80...
#... timeouts.

[EDIT] I was able to reproduce this commands in other machine and worked. It might be a older openstack version, since IPV6 support only came after Victoria release.

@yasminvalim yasminvalim force-pushed the openstack-support branch 3 times, most recently from 787e7b9 to 0bb5233 Compare March 6, 2025 19:42
@yasminvalim
Copy link
Contributor Author

image
Hey team, it's finally working properly and I was able to test it correctly. I would be grateful if you could take one last look at my code. Thanks!

Steps to Test:

  • Create the FCOS image with Ignition changes
  • Enter the OpenStack environment
  • Upload the modified FCOS image to your OpenStack environment.
  • Create the OpenStack image
  • Create the Ignition file
  • Prepare an Ignition configuration file that contains the necessary setup for your FCOS server.
    Use the following command to create the server, passing the Ignition file path as user-data:
openstack server create \
  --port <port> \
  --image fedora-coreos-example.20250211.dev.0-openstack.x86_64 \
  --flavor worker \
  --security-group default \
  --user-data <ignition_file_path> \
  <server_name>

Note: If you're using the Ignition file with SSH keys, you don't need to specify a keypair.

  • SSH into the server

Once the server is created, you can SSH into it using the following command:

ssh -o "IdentitiesOnly=yes" -i <private_key_path> core@<server_ip>

image

@yasminvalim yasminvalim force-pushed the openstack-support branch 2 times, most recently from 18f03e7 to b3d311d Compare March 7, 2025 17:41
@yasminvalim yasminvalim marked this pull request as ready for review March 7, 2025 17:43
Copy link
Collaborator

@prestist prestist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good start, and thank you for working on this, just a few questions and nits.

isIPv6 := ip.To4() == nil
return isIPv6
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to have some reference on why you are looking for the zone ID in the manner that you are. A simple link to the IPV6 metadata service docs should be good with a quick summary? wdyt?

return "", fmt.Errorf("no active IPv6 network interface found")
}

// Fetches configuration from both IPv4 and IPv6 metadata services
func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this whole function is a bit confusing in the direction it wants to go. The intent is to get our config from the metadata service. If a metadata service has our config we should be happy regardless of which one had it.

Currently, we are fetching two times even if we already have the data we need to continue.
I think we can restructure this to be a little more clear.

First we need to decide based on the issue which metadata service should be primary?
Then it falls into something like

fetch from primary 
if fail fetch from secondary

wdyt?

fmt.Println("Fetching zone id...")
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("error fetching zone id: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be more appropriate to say network interfaces as this is not really the zone id, and could confuse debugging. wdyt?

"error fetching network interfaces: %v", err

continue
}

for _, addr := range addrs {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I get a small comment explaining how this is giving us the zoneID?

@yasminvalim yasminvalim force-pushed the openstack-support branch 8 times, most recently from dbb2567 to e6b084a Compare March 13, 2025 14:47

// the metadata server exists but doesn't contain any actual metadata,
// assume that there is no config specified
if err == resource.ErrNotFound {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we continue handling the not found error like it was done here?
I believe the same would need to happen for IPv6.

Copy link
Contributor

@tormath1 tormath1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this and for the initial effort. This is less trivial than expected :D

Here's some feedback:

  1. The whole implementation (ipv4 / ipv6 detection) should be common to all providers. E.g: there is currently a similar ask for Scaleway provider (OpenStack single-stack IPv6 support #1897 (comment))
  2. For the implementation itself: it might increase the provisioning time as we need to wait for IPv4 to be seen as "unreachable" before giving up and trying IPv6
    duration := initialBackoff
    (not to mention that sometimes the interface itself is not directly available to the instance)

Something I would propose:

  1. For a cloud provider, we define a map of URLs:
urls := map[IP]url.URL{
    IPv4: url.URL{
        Scheme: "http",
	Host:   "169.254.169.254",
	Path:   "openstack/latest/user_data",
    },
    IPv6: url.URL{
	Scheme: "http",
	Host:   "[...]",
	Path:   "openstack/latest/user_data",
    }
}
  1. In a common implementation, we start two go routines sharing a success channel. One routine trying the IPv4 stack and the other one using the IPv6 stack (with the correct IP for each) - of course if the initial URLs map contains only IPv4, we start only the IPv4 function (same goes for IPv6).
  2. The first routine to return a success (i.e an Ignition configuration) will notify the channel and we can continue as usual (similar to what @prestist suggested here: https://github.com/coreos/ignition/pull/1909/files#r1985704411)

I'm not a big fan of using Go routines, but I think this situation might be a good usecase.

return nil, nil
// Try fetching from IPv4 first
response, err = f.FetchToBuffer(metadataServiceUrlIPv4, resource.FetchOptions{})
f.Logger.Debug("IPv6 URL:", metadataServiceUrlIPv6.Host)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f.Logger.Debug("IPv6 URL:", metadataServiceUrlIPv6.Host)
f.Logger.Debug("IPv4 URL:", metadataServiceUrlIPv6.Host)

We're trying to fetch IPv4 first?

@yasminvalim yasminvalim reopened this Apr 14, 2025
This defines a wrapper that will try in paralell both IPv4 and IPv6 when
the provider declares those two IPs.

Signed-off-by: Mathieu Tortuyaux <[email protected]>
@yasminvalim
Copy link
Contributor Author

yasminvalim commented Apr 14, 2025

Hey @tormath1, thanks for reviewing and working on that! I went through your changes, and your approach makes complete sense. I agree with your comments. To align with your work, I’ve cherry-picked the changes you made in internal/resource/url.go, and I’ll adapt the provider side accordingly so we can implement something like what you did for Scaleway for OpenStack!

Thanks again!

@yasminvalim
Copy link
Contributor Author

@tormath1 I just finished implementing the helper! I'd really appreciate it if you could take a look and share your feedback.

@tormath1
Copy link
Contributor

tormath1 commented May 12, 2025

@tormath1 I just finished implementing the helper! I'd really appreciate it if you could take a look and share your feedback.

Thanks for the ping! I somehow missed it. I'll give a try to this implementation, I noticed some hiccups in the current approach but I think it should be easy to fix. I'll let you know :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants