Skip to content
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

update internal DNS during blueprint execution #4989

Merged
merged 19 commits into from
Feb 21, 2024
Merged

Conversation

davepacheco
Copy link
Collaborator

@davepacheco davepacheco commented Feb 5, 2024

Fixes #4887.

@davepacheco davepacheco self-assigned this Feb 5, 2024
Comment on lines -381 to -397
let scrimlet_srv_records =
self.scrimlets.clone().into_iter().map(|(location, addr)| {
let srv = DnsRecord::Srv(dns_service_client::types::Srv {
prio: 0,
weight: 0,
port: addr.port(),
target: format!("{location}.scrimlet.{}", DNS_ZONE),
});
(ServiceName::Scrimlet(location).dns_name(), vec![srv])
});

let scrimlet_aaaa_records =
self.scrimlets.into_iter().map(|(location, addr)| {
let aaaa = DnsRecord::Aaaa(*addr.ip());
(format!("{location}.scrimlet"), vec![aaaa])
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

tl;dr: I brought this up in oxide-control-plane chat and sync'd with @rcgoodfellow and @internet-diglett and I believe these DNS names can be removed.

More details:

  • I could not find any references to these DNS names anywhere in Omicron. I looked for "switch0.scrimlet" and "switch1.scrimlet" and even ".scrimlet" (in case it was being filled into a template). If they were used programmatically, they'd show up as ServiceName::Scrimlet. The only place that's used is a comment explaining some context.
  • What I got from chat is what that comment says: these DNS names were added at some point after dogfood was set up. It was then discovered that they couldn't be used for whatever they were going to be used for because these DNS names only get set up at RSS-time, so systems like dogfood were just never going to have them. Instead, a different mechanism was created to find these zones when needed.
  • From what I can tell, the RSS code that assigned these DNS names chose the switch location (switch0 vs. switch1) arbitrarily, based on which one's sled id came lexicographically first. That's potentially inconsistent with what MGS reported as switch0 and switch1. So these names were potentially wrong on any systems where they were set up.
  • It should be possible to augment the code in this PR to reliably establish correct DNS names for these, provided we have an inventory collection with the information that we need from MGS. This way, in the future, we can use DNS for this. I didn't add this to this PR.

@@ -3082,6 +3082,11 @@ CREATE TABLE IF NOT EXISTS omicron.public.blueprint (
-- now gone.
parent_blueprint_id UUID,

-- identifies the latest internal DNS version when blueprint planning began
-- XXX-dap when it comes time for the migration, make sure to update
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When this PR has approval, I'll implement the db migration part. That part needs to make sure we provide a value for the internal_dns_version field for existing rows. I'm planning to set it to 1. That should be the current DNS version deployed everywhere. It should also be safe even if it weren't -- having one that's too old is always safe. It just means you won't be able to execute that blueprint and you need to re-plan to make a new one.

Comment on lines -324 to -327
let sled_address = get_sled_address(sled.subnet);
let switch_location = if i == 0 {
SwitchLocation::Switch0
} else {
SwitchLocation::Switch1
};
dns_builder.host_scrimlet(switch_location, sled_address).unwrap();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the bit I mentioned in my other comment. These DNS names were not used, and they were also not being assigned correctly.

@hawkw hawkw self-requested a review February 7, 2024 19:59
@davepacheco davepacheco marked this pull request as ready for review February 15, 2024 19:58
@davepacheco davepacheco removed the request for review from hawkw February 15, 2024 19:58
@hawkw hawkw self-requested a review February 15, 2024 20:04
Copy link
Contributor

@jgallagher jgallagher left a comment

Choose a reason for hiding this comment

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

This looks great; only small nits and clarification questions 👍


let example = example();
let error = DnsDiff::new(&example_empty, &example)
.expect_err("unexpectedly succeeded comparing two empty configs");
Copy link
Contributor

Choose a reason for hiding this comment

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

Tiny nit - wrong string on this expect I think? (Same on line 148 below)

self.sled_agents
.iter()
.filter(|(_, inventory)| inventory.sled_role == SledRole::Scrimlet)
.map(|(sled_id, _)| *sled_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

Tiny nit - if you want to squish this down to filter_map, you could use https://doc.rust-lang.org/std/primitive.bool.html#method.then_some (which I only learned about recently):

.filter_map(|(sled_id, inventory)| (inventory.sled_role == SledRole::Scrimlet).then_some(*sled_id))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is neat and I didn't know it. But I think I like what's here better in this case.

.unwrap();
}

dns_builder.generation(blueprint.internal_dns_version.next());
Copy link
Contributor

Choose a reason for hiding this comment

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

This threw me a bit, because it looks like the returned DNS config has always has a generation bump, which means it will be marching up every time this RPW runs. But making sure I understand: that doesn't happen because the config we return here is compared against the existing config using DnsDiff, which does not look at the generation number, only the contents, right? That seems a little implicit, but I don't have a great suggestion off the top of my head for making it clearer. We could wait to bump the generation until after the comparison, maybe? That might be a little more obvious?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see what you mean. I think it would be an improvement if instead of DnsConfigParams, we passed around the equivalent of dns_config_params.zones[0].records -- i.e., just the records that are in the internal DNS zone. Then we'd assemble that into a final DnsConfigParams at the last minute, before writing it to the database. Then it would be clearer what's being generated and diff'd, etc. But right now there's no type for that, just HashMap<String, Vec<DnsRecord>>. I think at minimum we'd want a newtype for this, and probably a richer one, too. DnsConfigBuilder maybe should produce one of those instead of DnsConfigParams. Anyway I think this would be better but I'd like to not do it right now.

I have at least added a few comments to explain this though.

(I inferred your suggestion to "wait to bump the generation until after the comparison" to mean "return something with the same generation as what's there, then do the comparison, then mutate it if we find we need to bump it". I don't love that. It seems misleading in a different way.)

continue;
}

if let Some(scrimlet_id) = switch_sleds_by_ip.remove(addr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just for my own understanding: if we remove the scrimlet ID the first time we get here for a particular address, why don't we trip up later due to switch zones having multiple DNS records (dendrite / mgs / mgd)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good question! I believe there are a few different records here:

  • one AAAA record for something like dendrite-$sled_id.control-plane.oxide.internal
  • three SRV records for stuff like _mgs._tcp.control-plane.oxide.internal, _dendrite._tcp.control-plane.oxide.internal, and _mgd._tcp.control-plane.oxide.internal. These have a target field which points at dendrite-$sled_id.control-plane.oxide.internal, not the IP address directly.

This loop iterates over just the names having AAAA records. There should be only one of those for each Scrimlet. It's the next loop that iterates over the SRV records and checks those.

use std::collections::BTreeMap;
use uuid::Uuid;

pub async fn deploy_dns<S>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Two tiny nits:

  • I'd make this pub(crate)
  • Given the former, I think you could drop <S> and take creator: String, and let the caller do the conversion. That keeps the ergonomics of the public API without needing the generic on this inner function.

@davepacheco davepacheco merged commit 1e76aca into main Feb 21, 2024
22 checks passed
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.

blueprints: Nexus needs to update internal DNS
2 participants