Skip to content

Add a4x2-package/a4x2-deploy xtasks#7424

Open
faithanalog wants to merge 54 commits into
mainfrom
artemis/a4x2-package
Open

Add a4x2-package/a4x2-deploy xtasks#7424
faithanalog wants to merge 54 commits into
mainfrom
artemis/a4x2-package

Conversation

@faithanalog

@faithanalog faithanalog commented Jan 29, 2025

Copy link
Copy Markdown
Contributor

overview

This PR adds two new xtasks:

  • a4x2-package
  • a4x2-deploy

a4x2-package

> cargo xtask a4x2-package -h
Generate a tarball with omicron packaged for deployment onto a4x2

Usage: cargo xtask a4x2-package [OPTIONS]

Options:
      --live-tests        Bundle omicron live tests into the output tarball
      --end-to-end-tests  Bundle omicron end-to-end-tests package into the output tarball
  -h, --help              Print help

This command will:

  • download & build the latest a4x2 from oxidecomputer/testbed
  • clone the local working tree to a temporary path & build + package it with a4x2
  • if requested, build live-tests
  • if requested, build end-to-end tests (but there is no way to run these yet)
  • package these all into a tar bundle, at out/a4x2-package-out.tgz

a4x2-deploy

> cargo xtask a4x2-deploy -h
Run a4x2 and deploy omicron onto it, and optionally run live-tests and end to end tests

Usage: cargo xtask a4x2-deploy <COMMAND>

Commands:
  start      Start a4x2, deploy a control plane to it, and then leave it running
  stop       Stop a4x2 that was previously launched with the `start` subcommand
  run-tests  Start a4x2, run tests, and then stop a4x2 unless you request otherwise
  help       Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

This command will:

  • tear down any existing a4x2 created by a previous invocation of a4x2-deploy
  • unpack a bundle generated by a4x2-package
  • download latest OVMF code & propolis server
  • launch a4x2
  • curl the control plane API until it becomes alive (or a long timeout is reached, in which case it considers the launch failed)
  • if requested, transfer live tests to the system and execute them
  • print information on how to access the server

By default, a4x2-deploy will use a bundle from out/a4x2-package.tgz, but an alternative can be specified with --package <path>, so you can build a package and send it to someone else for them to run on their machine.

Known shortcomings

  • a4x2 is still a bit flaky. Sometimes I will reboot my machine for good luck.
  • a4x2-deploy could and should automatically collect logs from the system when launches fail, but does not yet.
  • no support for incremental builds of omicron. a4x2 makes some changes to tomls in the current working directory, which is why we clone the source directory to another path prior to running the a4x2 package step.

Additionally, because of cloning the source to a temporary working path, any changes you have not git added will not be seen by a4x2-package. I consider this either mildly useful or mildly confusing depending on what you're doing in the moment. The jj crew can of course just run jj before using a4x2-package.

We could opt instead of doing a cp -a of the working directory instead of a git clone, but this could be problematic due to copying potentially massive target/ directories. We could use rsync with excludes, but I'm not sure we can rely on it always being present. The git clone seemed like the most reliable to me, and it has precedent in the releng xtask. But I do find this to be somewhat annoying when iterating on changes and am open to alternatives. Maybe we should just try to make a4x2 work in the current workdir without causing trouble.

Aside on xshell

This PR adds a dependency on xshell, which I use heavily for running system commands. I figured I would take the opportunity to talk about my experience with it, and provide some context that may be helpful when reviewing this PR. I've really enjoyed using it and think it makes it quite pleasant to use rust as a bash replacement.

A few things to keep in mind when reading it, which you can learn from the docs, but I will also highlight here:

All commands are echoed by default

Similar to set -x in bash, commands are echoed as they are run. This is great for logs. Additionally, non-zero exit codes become Errs, and command stdout/stderr are sent to the terminal by default, unless you make a point of capturing them.

Command invocations are not echoed when you use .read() to capture stdout

cmd!() is not sending your command into a shell

cmd!() is building a normal rust Command! If you see something like

cmd!(sh, "some command here {with_args}")

You may ask "will that escape with_args"? The answer is, there is no shell invocation, so there is nothing to escape! It is similar to as if you had done

Command::new("some").arg("command").arg("here").arg(&with_args)

xshell is mostly convenience around Command, with a few other file access shorthands thrown in.

sh.pushdir()

You will see a lot of a pattern

let _popdir = sh.pushdir(some_dir);

sh has its own internal working directory it stores (separate from the process workdir, btw). All commands / operations run through sh are relative to that working directory. pushdir changes that directory, and returns a guard. When that guard is dropped, the directory is popped.

You may be wary if you have bitten by code like this before:

// guard is immediately dropped, because thats what `let _ = ` does! Oops!
let _ = thing_that_returns_guard();

In the next version of xshell, it looks like pushdir is going away entirely, and will be replaced with approximately this:

sh.with_dir(some_dir, || {

})

I'll deal with that when it happens. I'm sure the lambdas will make some things more annoying, but I think it'll be better that way.

@faithanalog

Copy link
Copy Markdown
Contributor Author

@davepacheco tagged for review since we've been talking about this for awhile. would also tag ry, but he is out. not sure who else might be interested in reviewing.

@davepacheco

Copy link
Copy Markdown
Collaborator

Sorry for the delay in looking at this! I'm just starting to look now.

Additionally, because of cloning the source to a temporary working path, any changes you have not git added will not be seen by a4x2-package. I consider this either mildly useful or mildly confusing depending on what you're doing in the moment. The jj crew can of course just run jj before using a4x2-package.
...
Maybe we should just try to make a4x2 work in the current workdir without causing trouble.

I think you've made the right call here in using Git. I also think making a4x2 work in the current work tree is a great goal, but shouldn't block this work. In the meantime, I'd suggest that the tool fail by default with a clear message if the working tree is dirty, but allow that to be overridden.

@davepacheco davepacheco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is looking great! The description in the PR was very helpful in getting oriented -- thanks for that.

I haven't yet looked at a4x2-deploy.

Comment thread dev-tools/xtask/Cargo.toml
Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated

#[derive(Deserialize, Debug)]
struct NextestConfigVersion {
// required: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove?

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +41 to +52
struct Environment {
cargo: String,
home_dir: Utf8PathBuf,
work_dir: Utf8PathBuf,
src_dir: Utf8PathBuf,
out_dir: Utf8PathBuf,
omicron_dir: Utf8PathBuf,
in_ci: bool,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some doc comments here would be a big help.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
// Get a ref we can checkout from the local working tree
let treeish = {
let _popdir = sh.push_dir(&env.src_dir);
let mut treeish = cmd!(sh, "git stash create").read()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is where I'd be inclined to:

  • check if the working tree is dirty
  • if so: bail unless some --allow-dirty is specified
  • regardless, use HEAD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also:

  • Use $GIT before git.
  • Altering stash-related state is a visible operation and I don't think we should do it -- instead, there are ways to create a "background" commit from the current state.
  • What happens if this is run in a non-git repo, e.g. a non-colocated Jujutsu repo?

Another option that has somewhat less source control interaction is:

  • get the list of all tracked files in the repo (here's instructions on how)
  • create hardlinks for all of them in a mirrored structure. (Hardlinks are fine because basically all editors write files atomically, which breaks them.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Altering stash-related state is a visible operation and I don't think we should do it

git stash create (not to be confused with git stash push) doesn't put the created commit object anywhere in the ref namespace, per the man page -- is it visible in some other way that creating a tree with git update-index etc isn't?

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +158 to +168
// I'm not confident this does what it should, because I don't know much
// about the end to end tests. This is just going from what the shell script
// did.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@iliana maybe could answer this?

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +183 to +186
// XXX right now we only download a new nextest in CI, and we use
// the user's locally installed nextest otherwise. I'm inclined to
// say we should always download a fresh one for consistency.
// thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// XXX right now we only download a new nextest in CI, and we use
// the user's locally installed nextest otherwise. I'm inclined to
// say we should always download a fresh one for consistency.
// thoughts?
// XXX right now we only download a new nextest in CI, and we use
// the user's locally installed nextest otherwise. I'm inclined to
// say we should always download a fresh one for consistency.
// thoughts?

I'd be inclined to have this tool assume that the user has already set up nextest however they want. That's what the rest of Omicron currently does, I believe. If CI needs to download it, that's fine, but it should do it as a separate step before invoking this tool (which I think is what it does for the existing CI jobs).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed -- CI already downloads nextest. I'd just package up whatever nextest is available in the PATH.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +269 to +270
// XXX use main branch
// cmd!(sh, "git -C {testbed_dir} checkout artemis/init-in-smf").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could make this a command-line argument. It could accept a branch name or alternatively a local path to a clone if you want to do anything other than "main".

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
pub fn run_cmd(args: A4x2DeployArgs) -> Result<()> {
let sh = Shell::new()?;

// TODO: do we want to have a configurable falcon dataset?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes, definitely. I can't run a4x2 on my system without overriding the default here.

It'd be nice to override a few other things, like THREE_NODE_RSS. Not a blocker though for v1.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agreed. This should be usable on shared systems like gimlozar, where more than one engineer need to be able to do this in parallel without clobbering one another.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
// Delete any results from previous runs. We don't mind if
// there's errors. This needs to run as root in case there are
// artifacts owned by root left around from the deploy.
cmd!(sh, "pfexec rm -rf").arg(&env.work_dir).run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is (obviously) incredibly dangerous. I wonder how we can make it less so.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, agreed -- if you could set work_dir to something like /etc (e.g. via the CLI option discussed above) you could nuke the whole system.

How many files is this typically, and how many are owned by root? If it's not too many, I'm wondering if we could do a first pass that iterates over the file system to find files owned by root, and then lists out those files. Then the user can be prompted to ask if it's okay to use pfexec (and there can be a --force option to skip prompting.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What goes in here? Why do you need pfexec to remove it?

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +226 to +252
// Generate an ssh key we will use to log into the sleds.
cmd!(sh, "ssh-keygen -t ed25519 -N '' -f a4x2-ssh-key").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ooh, (I think) I like using a one-off key here. But it also needs to be easy for folks to ssh in after the fact. Should we give folks a command to load it into their agent?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Or provide an ssh command to copy-paste.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it makes sense to inject a key that you can reliably use locally from the automation here, but I would want to continue using my yubikey-backed SSH keys to actually log in -- we should be willing to import keys from some other place, such as ~/.ssh/authorized_keys, or I guess potentially from the output of ssh-add -L, etc.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
.find(|v| v["dynamic"] == Value::Bool(true))
.ok_or(anyhow!("failed to find customer edge addr"))?["local"]
.as_str()
.unwrap();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

May as well replace these unwrap()s with error messages?
Would it make sense to define a Rust struct that derives Deserialize so that if any of the assumptions in ce_addr_json[0]["addr_info"].as_array().unwrap() aren't true, we fail gracefully? (value might not be an array; might not have addr_info property, it might not be an array, etc.)

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +271 to +299
// XXX Im told that pinging the gateway from inside the sleds is no longer
// necessary so I'm leaving it out for now. We'll see if that's true.
// So far it seems to be.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// XXX Im told that pinging the gateway from inside the sleds is no longer
// necessary so I'm leaving it out for now. We'll see if that's true.
// So far it seems to be.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
// necessary so I'm leaving it out for now. We'll see if that's true.
// So far it seems to be.

cmd!(sh, "pfexec route add 198.51.100.0/24 {customer_edge_addr}").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm surprised you have to do this because I don't remember doing it. Where does this happen during a manual a4x2 launch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(and more generally, could there be a reference to documentation for this function so we can look at things step-by-step?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Presumably you only need to do this if you're actually trying to directly communicate with that subnet from outside; i.e., from processes you run on the host alongside this.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +288 to +335
// The important thing here is to do an HTTP request with timeout to the
// control plane API endpoint. If the server is up, we'll get the page you'd
// expect for an unauthenticated user. If not, then the request will either
// - fail
// - stall
//
// Stalling is why we do a timeout, and we need to handle timeout both at
// the TCP level and the HTTP level. `curl -m 5` will hard-out after 5
// seconds no matter what's going on.
//
// That 5 seconds is arbitrary, but it's been working well over in
// rackletteadm, from which this logic is copied.
//
// We could replace curl with rust code, but make sure we do the same thing!
while retries > 0 && cmd!(sh, "curl -s -m 5 {api_url}").run().is_err() {
retries -= 1;
thread::sleep(time::Duration::from_secs(25));

if retries % 5 == 0 {
// I see no reason to error while printing the date
let _ = cmd!(sh, "date").run();
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder if you can use the same code that the end-to-end tests use. I seem to recall them doing the same thing in Rust.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we do that, then it's worth factoring that out so we don't have to build e.g. our Diesel code to build this binary. (And if we do that, we should definitely put this into an external binary.)

@sunshowers sunshowers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for doing this! Really excited about automating all of this.

(I'm still working on the review -- here's what I have for now.)

Comment thread dev-tools/xtask/Cargo.toml Outdated
usdt.workspace = true
walkdir.workspace = true
xshell.workspace = true
sha2.workspace = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you put this in sorted order? Thanks!

Comment thread dev-tools/xtask/Cargo.toml
Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +123 to +150
let result = teardown_a4x2(&sh, &env);
eprintln!("teardown result: {:?}", result);
eprintln!("continuing regardless of whether there were errors");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this print out the ok and error outputs nicely? Then the error can be printed conditionally.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
// Delete any results from previous runs. We don't mind if
// there's errors. This needs to run as root in case there are
// artifacts owned by root left around from the deploy.
cmd!(sh, "pfexec rm -rf").arg(&env.work_dir).run()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, agreed -- if you could set work_dir to something like /etc (e.g. via the CLI option discussed above) you could nuke the whole system.

How many files is this typically, and how many are owned by root? If it's not too many, I'm wondering if we could do a first pass that iterates over the file system to find files owned by root, and then lists out those files. Then the user can be prompted to ask if it's okay to use pfexec (and there can be a --force option to skip prompting.)

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +380 to +406
cmd!(sh, "pfexec route delete 198.51.100.0/24 {gateway}")
.run()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth printing out the fact that we're doing this.

In general I really like the approach where we first gather all operations into a structure, and then perform them in a separate pass. That enables prompting for confirmation, and things like --dry-run. Not sure if you want to put the effort into doing that, but if you do I'd really appreciate it :)

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +387 to +414
if !had_gateway {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is had_gateway not being present an exceptional condition? Worth adding a comment in any case.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +409 to +435
// pour one out for enjoyers of clean indentation
println!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

https://docs.rs/indoc if you'd like to use it!

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
"teardown_a4x2: could not get gateway for a4x2 route from line {ln}"
))?;

cmd!(sh, "pfexec route delete 198.51.100.0/24 {gateway}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like some of these IPs and subnets are fixed and well-known -- could you make them constants at the top of the file?

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +456 to +482
let ipv4 = ln.split_whitespace().nth(3).ok_or(anyhow!("get_host_ip: could not extract IP for node {node} from line {ln}"))?;
let ipv4 = ipv4.strip_suffix("/24").ok_or(anyhow!("get_host_ip: could not extract IP for node {node} from line {ln}"))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same comments as in the gateway section above.

@jclulow jclulow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems like a great idea! I have a hodge podge of thoughts I've left as comments, but I wanted to say I agree this makes things much more accessible to casual users in particular. I also like the concrete separation of build machine and target machine, with an artefact you can pass between them!

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
pub fn run_cmd(args: A4x2DeployArgs) -> Result<()> {
let sh = Shell::new()?;

// TODO: do we want to have a configurable falcon dataset?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agreed. This should be usable on shared systems like gimlozar, where more than one engineer need to be able to do this in parallel without clobbering one another.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +103 to +126
} else {
home_dir.join(".cache/a4x2-deploy")
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How big does this cache/work directory get in practice?

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
// all of them. But, I don't like unbounded loops, so we will do at max 10,
// which is a number that seems unlikely enough to reach, to me.
for _ in 0..10 {
let mut route_cmd = cmd!(sh, "pfexec route get 198.51.100.0/24");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let mut route_cmd = cmd!(sh, "pfexec route get 198.51.100.0/24");
let mut route_cmd = cmd!(sh, "route -n get 198.51.100.0/24");

You don't need elevated privileges to route get, and we should use the -n flag to avoid name service lookups.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
}

pub fn run_cmd(args: A4x2DeployArgs) -> Result<()> {
let sh = Shell::new()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should probably have a common wrapper around Shell::new(), which should at minimum scrub the environment that these commands are going to run in. In particular, I think we should make sure no LC_* or LANG variables are inherited, and instead specify LANG='C.UTF-8' for all invocations so that we get consistent output from commands and so on.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +333 to +371
// If you want any change in functionality for the test runner, update
// run-live-tests over in a4x2_package.rs. Don't add it here!
let switch_zone_script = r#"
set -euxo pipefail
tar xvzf live-tests-bundle.tgz
cd live-tests-bundle
./run-live-tests
"#;

let remote_script =
format!("zlogin oxz_switch bash -c '{switch_zone_script}'");

// Will error if the live tests fail. This is desired.
cmd!(sh, "ssh {ssh_args...} {ssh_host} {remote_script}").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel like you're getting extremely lucky with the several layers of belligerent escaping that occur when descending through all of xshell, SSH, zlogin, and then bash -c, haha.

I don't have any specific guidance other than it feels like this will fall down if there end up being any quotes or load bearing whitespace required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

xshell doesn't invoke shell, it's just rust Command, so no escaping necessary there. ssh will feed the zlogin command as is to the shell so that's also fine.

there is one place that this is at risk of escape behavior, which is if the script contains any single-quotes in it. I can fix that with ye olde replace("'", "'\"'\"'") and then it will be clean all the way thru even if someone starts changing the script in arbitrary ways.

but I agree that I am not thrilled by what's going on here.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
"teardown_a4x2: could not get gateway for a4x2 route from line {ln}"
))?;

cmd!(sh, "pfexec route delete 198.51.100.0/24 {gateway}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
cmd!(sh, "pfexec route delete 198.51.100.0/24 {gateway}")
cmd!(sh, "pfexec route -n delete 198.51.100.0/24 {gateway}")

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +367 to +393
// We get an error code when there is no route (and thus, nothing for
// us to delete!)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think before ignoring the result here I would make sure the output ends in not in table, otherwise it's presumably an error we're not expecting and we'd like to display it to the user and abort.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +171 to +199
// We ignore any error here, because the result we
// actually want to produce is whether tests passed
let _ = teardown_a4x2(&sh, &env);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we should ignore this error. Either teardown_a4x2() should work, or we need to stop and figure out why it did not.

Comment thread dev-tools/xtask/src/a4x2_deploy.rs Outdated
Comment on lines +120 to +148
// Teardown previous deploy if it exists, before wiping the data
// for it. If this errors, we assume the deploy doesn't exist
// and carry on.
let result = teardown_a4x2(&sh, &env);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we should ignore this error. Either teardown_a4x2() should work, or we need to stop and figure out why it did not.

@sunshowers sunshowers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks again -- went through and left a few more comments.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
let src_dir = Utf8PathBuf::try_from(env::current_dir()?)?;

// Delete any results from previous runs. We don't mind if there's errors.
fs::remove_dir_all(&work_dir).ok();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also worth reporting errors here as a warning.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
use serde::Deserialize;
use sha2::Digest;
use std::env;
use std::fs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider https://docs.rs/fs-err for better error messages.

Comment thread dev-tools/xtask/src/main.rs Outdated
Comment on lines +16 to +17
mod a4x2_deploy;
mod a4x2_package;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suspect we'll want to share some code between the two -- what do you think about putting them in an a4x2 module and having a4x2/deploy.rs and package.rs?

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
// Get a ref we can checkout from the local working tree
let treeish = {
let _popdir = sh.push_dir(&env.src_dir);
let mut treeish = cmd!(sh, "git stash create").read()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also:

  • Use $GIT before git.
  • Altering stash-related state is a visible operation and I don't think we should do it -- instead, there are ways to create a "background" commit from the current state.
  • What happens if this is run in a non-git repo, e.g. a non-colocated Jujutsu repo?

Another option that has somewhat less source control interaction is:

  • get the list of all tracked files in the repo (here's instructions on how)
  • create hardlinks for all of them in a mirrored structure. (Hardlinks are fine because basically all editors write files atomically, which breaks them.)

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
if treeish.is_empty() {
// nothing to stash
eprintln!("Nothing to stash, using most recent commit for clone");
treeish = cmd!(sh, "git rev-parse HEAD").read()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again these should use $GIT.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +330 to +326
if can_dedupe {
// yay, extract out to the common dir.
let dest = common_dir.join(base_path);
sh.create_dir(&dest.parent().unwrap())?;
sh.copy_file(&g0_path, &dest)?;
sh.remove_path(&g0_path)?;
for prefix in &prefixes {
sh.remove_path(&prefix.join(base_path))?;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm interesting -- what code ensures that sled-common is checked?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In a4x2-deploy, I've got this block of commands

    cmd!(sh, "tar -cf sled-common.tar -C {a4x2_dir}/cargo-bay/sled-common ./")
        .run()?;
    cmd!(sh, "tar -xf sled-common.tar -C {a4x2_dir}/cargo-bay/g0").run()?;
    cmd!(sh, "tar -xf sled-common.tar -C {a4x2_dir}/cargo-bay/g1").run()?;
    cmd!(sh, "tar -xf sled-common.tar -C {a4x2_dir}/cargo-bay/g2").run()?;
    cmd!(sh, "tar -xf sled-common.tar -C {a4x2_dir}/cargo-bay/g3").run()?;

This

  • creates an intermediate tar of the common dir
  • extracts it into each of g0/g1/g2/g3's individual directories

which, since tar does not delete any of the files in g{0,1,2,3}, this simply adds them in place in addition to anything else there

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
Comment on lines +202 to +204
// XXX do we need to test nextest exists & is the right version? need to
// see if the incidental errors from stuff below already take care of
// telling someone outside of CI what they need to be doing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could run cargo nextest show-config version -- exit code 92 means that the required version isn't met, and exit code 10 means that the recommended version isn't met.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
// intentionally relative path so the tarball gets relative paths
let _popdir = sh.push_dir(&env.work_dir);
let bundle_dir_name = live_test_bundle_dir.file_name().unwrap();
cmd!(sh, "tar -czf {out_dir}/live-tests-bundle.tgz {bundle_dir_name}/")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

live-tests-bundle.tgz maybe can be a constant.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
let pkg_dir_name = env.out_dir.file_name().unwrap();

// Place output in `out/` when running locally, or leave in work dir for CI
let artifact = Utf8PathBuf::from("a4x2-package-out.tgz");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This filename can be a constant too, I think.

Comment thread dev-tools/xtask/src/a4x2_package.rs Outdated
let a4x2_dir = testbed_dir.join("a4x2");

// TODO move this clone & build to prepare, so that if the omicron build
// takes awhile we wont have lost our git token when we get here. Relevant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// takes awhile we wont have lost our git token when we get here. Relevant
// takes awhile, we won't have lost our git token when we get here. Relevant

@faithanalog faithanalog force-pushed the artemis/a4x2-package branch 2 times, most recently from 881e46b to 2bd1eb4 Compare February 25, 2025 01:55
@faithanalog

faithanalog commented Mar 3, 2025

Copy link
Copy Markdown
Contributor Author

Ran this in a loop over the weekend. I got about 270 executions. Some stats:

  • a4x2 fails 20% of the time. meaning: a4x2 executable returns non-zero exit code, some VM never turned on or similar occurrence
  • When a4x2 succeeds, control plane fails 11.68% of the time. meaning: a4x2 turned on fine, but we timed out waiting for the API to come up.
  • When control plane succeeds, live tests fail 4.76% of the time. Meaning, control plane came up, live tests ran, but returned a failure. This one might be my fault; I launch live tests immediately once I get a 200 OK response from the nexus external API endpoint, and maybe it actually needs a little extra time for something else to happen before it's in a state where the live test works. But I don't know how to confirm this.

Overall this means that we have a pass rate of 67% / failure rate of 33%.

Also, timing information, with regards to our timeouts:

When a4x2 fails (remember, we never get to polling in this case), a4x2 fails after 10 minutes on average, with a max of 11 minutes. That's reasonable, I think, so I don't feel compelled to add a timeout in the xtask to kill a4x2 if it takes too long.

If we do pass a4x2, then on average, it takes 10 minutes of polling before the control plane is up. The longest poll was 16:20. So my 25 minute poll timeout can be made more aggressive- at least on my machine. It may be faster or slower on others.

In total, the average fully-successful (tests passing) run took 21:32, and the longest took 27:47.

@jgallagher

Copy link
Copy Markdown
Contributor

When a4x2 succeeds, control plane fails 11.68% of the time. meaning: a4x2 turned on fine, but we timed out waiting for the API to come up.

FWIW (maybe very little!): #7712 fixes a problem that has caused this failure mode for me in a4x2 with some regularity. This was a real bug that could occur on real systems, but is much less likely on a healthy system. I wonder how much of this 11.68% is either that exact issue or something similarly possible-albeit-unlikely on real hardware.

@faithanalog faithanalog force-pushed the artemis/a4x2-package branch from 51f0afd to 32701d0 Compare March 13, 2025 23:34
faithanalog and others added 17 commits March 16, 2025 19:50
This PR adds two new xtasks:

- a4x2-package
- a4x2-deploy

```
> cargo xtask a4x2-package -h
Generate a tarball with omicron packaged for deployment onto a4x2

Usage: cargo xtask a4x2-package [OPTIONS]

Options:
      --live-tests        Bundle omicron live tests into the output tarball
      --end-to-end-tests  Bundle omicron end-to-end-tests package into the output tarball
  -h, --help              Print help
```

This command will:

- download & build the latest a4x2 from oxidecomputer/testbed
- clone the local working tree to a temporary path & build + package it with a4x2
- if requested, build live-tests
- if requested, build end-to-end tests (but there is no way to run these yet)
- package these all into a tar bundle, at `out/a4x2-package-out.tgz`

```
> cargo xtask a4x2-deploy -h
Run a4x2 and deploy omicron onto it, and optionally run live-tests and
end to end tests

Usage: cargo xtask a4x2-deploy <COMMAND>

Commands:
  start      Start a4x2, deploy a control plane to it, and then leave it running
  stop       Stop a4x2 that was previously launched with the `start` subcommand
  run-tests  Start a4x2, run tests, and then stop a4x2 unless you request otherwise
  help       Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
```

This command will:

- tear down any existing a4x2 created by a previous invocation of a4x2-deploy
- unpack a bundle generated by a4x2-package
- download latest OVMF code & propolis server
- launch a4x2
- ping the control plane until it becomes alive (or a timeout is reached,
  in which case it considers the launch failed)
- if requested, transfer live tests to the system and execute them
- print information on how to access the server

By default, a4x2-deploy will use a bundle from `out/a4x2-package.tgz`,
but an alternative can be specified with `--package <path>`, so you can
build a package and send it to someone else for them to run on their
machine.

- a4x2 is still a bit flaky. Sometimes I will reboot my machine for good luck.
- a4x2-deploy could and should automatically collect logs from the
  system when launches fail, but does not yet.
- no support for incremental builds of omicron. a4x2 makes some changes to
  tomls in the current working directory, which is why we clone the source
  directory to another path prior to running the a4x2 package step.

Additionally, because of cloning the source to a temporary working path,
any changes you have not `git add`ed will not be seen by `a4x2-package`.
I consider this either mildly useful or mildly confusing depending on
what you're doing in the moment. The jj crew can of course just run `jj`
before using `a4x2-package`.

We could opt instead of doing a `cp -a` of the working directory instead
of a git clone, but this could be problematic due to copying potentially
massive `target/` directories. We could use `rsync` wtith excludes, but
I'm not sure we can rely on it always being present. The `git clone`
seemed like the most reliable to me, and it has precedent in the
`releng` xtask. But I do find this to be somewhat annoying when
iterating on changes.
(as discussed over coffee)
@faithanalog faithanalog force-pushed the artemis/a4x2-package branch from 665561c to 2206066 Compare March 17, 2025 02:50
@faithanalog

Copy link
Copy Markdown
Contributor Author

I am declaring github UX bankruptcy for replying to all the comment threads one by one, but to the best of my knowledge I've addressed everything. But, it has been a lot of changes, so I will not be surprised if folks raise new issues.

Primary changes since initial PR:

work is stored in target/

No more stuffing things in ~/.cache. This also makes it cargo clean-able. Nothing is owned by root, so I'm not doing any pfexec rm anymore.

As root-owned files go, the only thing that was owned by root was the files created by falcon when running pfexec a4x2 launch. Right now I'm solving that problem by chown-ing them to the current user after launch runs. It might be nicer if falcon could be run without root and elevate itself as needed; then it could create those files in advance as a non-root user. I'm putting that under potential future improvements.

copying omicron source code

I switched to using rsync instead of git. This matches up with how I actually do things in my personal build scripts, and have done for some time.

The actual requirements were:

  • clone the source tree, preserving file permissions, symlinks, etc.
  • don't copy out/ or target/

It's a minor downside that rsync is not installed by default on a helios box, but I think it is common enough to be ok to use here. I also considered doing a tar | tar, but I didn't trust myself to use illumos tar correctly.

no more in_ci

everything has either been made to work one way only, or turned into a command line flag.

we can collect logs from a4x2 now

somehow I forgot to actually implement this logic the first go around... it's there now as xtask a4x2 deploy collect-evidence. I collect falcon logs and all the logs I thought to grab from inside the sleds.

deploy usage

deploy is structured more like a developer tool that CI will run, rather than a CI tool that developers will run. This primarily means, one command per action:

Usage: cargo xtask a4x2 deploy <COMMAND>

Commands:
  install-propolis  Download and install the propolis hypervisor and bootcode. Pre-requisite for launching a4x2
  start             Start a4x2, deploy a control plane to it, and then leave it running. Destroys any existing a4x2 deployment, unless --ensure is used
  stop              Stop a4x2 that was previously launched with the `start` subcommand
  run-live-tests    Execute live-tests previously built by `xtask a4x2 package`
  collect-evidence  Collect logs from a4x2, and services within the sleds
  status            Query a4x2 node access information

This allows some flexibility for how to run things if using this tool while iterating. The usual flow would be

cargo xtask a4x2 deploy start \
    && cargo xtask a4x2 deploy run-live-tests

cargo xtask a4x2 deploy collect-evidence

cargo xtask a4x2 deploy stop

And in fact this is what CI will do. But if you instead wanted to run a4x2 and do something other than run bundled live tests, you can do that too. Previously I was making this possible with command line arguments to start, but this seemed less awkward to me.

This also means that deploy start polling could be made optional if we wanted. I left it as unconditional because I always want the command to poll for the control plane and tell me if it came up or not, and nobody has complained about the similar behavior in rackletteadm.

Other changes

  • a4x2 commands moved to submodule to lift common code/constants out to a mod.rs
  • a4x2 is a subcommand
    • a4x2-deploy -> a4x2 deploy
    • a4x2-package -> a4x2 package
  • lots of additional comments to address things people asked questions about in review
  • lots of context()
  • fs_err
  • allow specifying testbed source/branch
  • add user's ssh pubkeys to sleds
  • reduce control plane polling window to 20 minutes. Hopefully this isn't a problem for slower systems?
  • print remaining time while polling
  • env scrubbing
  • route -n
  • longopts for curl
  • Deserialize instead of adhoc json parsing
  • always build live-tests / end-to-end tests

Known issues / annoyances

install-propolis temporarily busted

xtask deploy install-propolis will install OVMF, but I have forbidden it from installing propolis for now. See oxidecomputer/falcon#106 for details; this is something I want to fix before merging.

i am always compiling omicron at all hours

While packaging, we will rebuild omicron-package and some parts of omicron multiple times. This is a new issue I picked up during a rebase! It's because of ring, like #7740. I don't think this is on me; I'm filtering out the offending env vars on my executions of things. It might be that omicron-package needs to filter the variables out, I am not sure. It does not break things, just makes things much slower than they ought to be for the time being.

git clone https:// my nemesis

Because the default testbed source path is https, folks who usually clone by git (such as myself) may get prompted for username/password. It's the first thing that happens when running a4x2 package, so it's at least not a nasty surprise to come back to after leaving the computer for a bit. I've just been specifying the git version of the source path while testing, but I want to figure out how to not need to do that.

if propolis core-dumps that's annoying

If propolis-server core dumps it does it in the work directory, and then you have a file owned by root you need to manually delete. I'm tempted to call this one out of scope, but I could look for a core file and add it to the list of things I chown if folks want.

@jclulow

jclulow commented Mar 18, 2025

Copy link
Copy Markdown
Collaborator

I don't think I have time at the moment for another review pass, but I've read your comment just now and the direction seems good to me! Because it's an engineering tool, rather than a shipped artefact, I think it's probably good to get this integrated and get some miles on it so we can continue to evolve it once people (and CI) are using it!

@davepacheco davepacheco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very nice! I also haven't had a chance to look as closely but agree with Joshua's comments that we're close enough at this point and the risk is low enough that I think we should land it sooner rather than later.

The two things I'd maybe consider blockers are:

  • this could use some instructions, either in README.adoc in the Omicron root or else a new docs/how-to-run-a4x2.adoc (alongside the existing how-to-run{,-simulated} docs)
  • speaking for myself, I need to be able to use FALCON_DATASET -- does that get honored here if I set it in my environment?


/// This script will be placed in the live tests bundle to run the live tests
const LIVE_TESTS_EXECUTION_SCRIPT: &str = r#"
set -euxo pipefail

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I like to use explicit names here so readers don't have to have these all memorized (e.g., set -o errexit).


// Delete any results from previous runs.
if work_dir.try_exists()? {
fs::remove_dir_all(&work_dir)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: print a message saying it's doing this

fs::remove_dir_all(&work_dir)?;
}

// Create work dir

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same -- print a message saying it's doing this ("using working directory {work_dir}")

fs::create_dir_all(&work_dir)?;
sh.change_dir(&work_dir);

// Output. Maybe in CI we want this to be /out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Output. Maybe in CI we want this to be /out
// Output.

let _popdir = sh.push_dir(&env.omicron_dir);

let cargo = &env.cargo;
cmd!(sh, "{cargo} build -p end-to-end-tests --bin commtest --bin dhcp-server --release").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why --release? I just wonder if leaving that off would improve runtime.

Comment on lines +265 to +267
cmd!(sh, "./config/build-packages.sh")
.env("OMICRON", &env.omicron_dir)
.run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will this honor env vars I've set (like FALCON_DATASET)?

/// the command will *ensure* a4x2 is running, starting it if needed, but
/// leaving an existing deployment alone.
#[clap(long)]
ensure: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: call this --if-not-running

Comment on lines +119 to +124
// ensure arg says: make sure a4x2 is in theory deployed.
// right now we will use the presence of .falcon to indicate
// that, but that can be an incorrect if the system rebooted
// since a4x2 was deployed.
eprintln!(".falcon directory exists; assuming a4x2 is up");
return Ok(());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems like this isn't reliable in that it will only not tear down if the falcon directory is present. But it might be present, yet nothing is running. I wonder: what's the motivation for this flag?


// Generate an ssh key we will use to log into the sleds.
// `-N ''` means "empty passphrase"
cmd!(sh, "ssh-keygen -t ed25519 -N '' -f a4x2-ssh-key").run()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd suggest printing a one line summary of what we did here. Something like loaded {n} keys from $HOME/.ssh

@davepacheco

Copy link
Copy Markdown
Collaborator

I keep thinking about this PR and trying to get an a4x2 setup going in CI. Is this work still ongoing?

@jclulow

jclulow commented Jul 29, 2025

Copy link
Copy Markdown
Collaborator

I think it would be good to try the new Gimlet-backed bare metal environment for this, now that it's available. I will try adding a change to the branch to do that.

@jclulow

jclulow commented Jul 29, 2025

Copy link
Copy Markdown
Collaborator

Oh, there is no actual CI job added with this, right, it's just tools?

@davepacheco

Copy link
Copy Markdown
Collaborator

Yes, I believe this PR just adds the tools that we would then use in a CI job.

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.

7 participants