Skip to content

Commit cd6952f

Browse files
ytsssunKCSesh
authored andcommitted
apiclient: add network configure subcommand
Implements 'apiclient network configure <input-source>' to enable runtime network configuration for Bottlerocket. stdin is the default input, but also supports file://, base64: input sources with client-side URI processing. Content is sent to the API server for further processing. Signed-off-by: Yutong Sun <yutongsu@amazon.com> Signed-off-by: Kyle Sessions <kssessio@amazon.com>
1 parent 33868e2 commit cd6952f

File tree

5 files changed

+523
-8
lines changed

5 files changed

+523
-8
lines changed

sources/api/apiclient/README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Current version: 0.1.0
55
## apiclient binary
66

77
The `apiclient` binary provides high-level methods to interact with the Bottlerocket API.
8-
There's a [set](#set-mode) subcommand for changing settings, an [update](#update-mode) subcommand for updating the host, and an [exec](#exec-mode) subcommand for running commands in host containers.
8+
There's a [set](#set-mode) subcommand for changing settings, a [network configure](#network-configure) subcommand for configuring network settings, an [update](#update-mode) subcommand for updating the host, and an [exec](#exec-mode) subcommand for running commands in host containers.
99
There's also a low-level [raw](#raw-mode) subcommand for direct interaction with the HTTP API.
1010

1111
It talks to the Bottlerocket socket by default.
@@ -75,6 +75,52 @@ You can use JSON form to set it:
7575
apiclient set --json '{"motd": "42"}'
7676
```
7777

78+
### Network configure
79+
80+
This allows you to configure network settings by providing a network configuration file. The configuration will be applied at the next boot.
81+
82+
The `network configure` command accepts input from different sources using URI schemes:
83+
84+
#### File URI input
85+
86+
You can specify a local file path using the `file://` URI scheme:
87+
88+
```shell
89+
apiclient network configure file:///path/to/net.toml
90+
```
91+
92+
#### Base64 encoded input
93+
94+
For inline configuration, you can provide base64-encoded content:
95+
96+
```shell
97+
apiclient network configure base64:dmVyc2lvbiA9IDIKCltldGgwXQpkaGNwNCA9IHRydWU=
98+
```
99+
100+
This is particularly useful for automation and configuration management where you want to embed the network configuration directly in scripts or user data.
101+
102+
#### Configuration format
103+
104+
The `net.toml` file uses the same format that netdog supports. Here's an example:
105+
106+
```toml
107+
version = 2
108+
109+
[eth0]
110+
dhcp4 = true
111+
dhcp6 = false
112+
113+
[eth1]
114+
static4 = ["192.168.1.100/24"]
115+
route = [{to = "default", via = "192.168.1.1"}]
116+
```
117+
118+
After configuring the network settings, you'll need to reboot the system for the changes to take effect:
119+
120+
```shell
121+
apiclient reboot
122+
```
123+
78124
### Update mode
79125

80126
To start, you can check what updates are available:
@@ -347,7 +393,7 @@ The results from each item in the report will be one of:
347393
## apiclient library
348394

349395
The apiclient library provides high-level methods to interact with the Bottlerocket API. See
350-
the documentation for submodules [`apply`], [`exec`], [`get`], [`reboot`], [`report`], [`set`],
396+
the documentation for submodules [`apply`], [`exec`], [`get`], [`network`], [`reboot`], [`report`], [`set`],
351397
and [`update`] for high-level helpers.
352398

353399
For more control, and to handle APIs without high-level wrappers, there are also 'raw' methods

sources/api/apiclient/README.tpl

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Current version: {{version}}
55
## apiclient binary
66

77
The `apiclient` binary provides high-level methods to interact with the Bottlerocket API.
8-
There's a [set](#set-mode) subcommand for changing settings, an [update](#update-mode) subcommand for updating the host, and an [exec](#exec-mode) subcommand for running commands in host containers.
8+
There's a [set](#set-mode) subcommand for changing settings, a [network configure](#network-configure) subcommand for configuring network settings, an [update](#update-mode) subcommand for updating the host, and an [exec](#exec-mode) subcommand for running commands in host containers.
99
There's also a low-level [raw](#raw-mode) subcommand for direct interaction with the HTTP API.
1010

1111
It talks to the Bottlerocket socket by default.
@@ -75,6 +75,52 @@ You can use JSON form to set it:
7575
apiclient set --json '{"motd": "42"}'
7676
```
7777

78+
### Network configure
79+
80+
This allows you to configure network settings by providing a network configuration file. The configuration will be applied at the next boot.
81+
82+
The `network configure` command accepts input from different sources using URI schemes:
83+
84+
#### File URI input
85+
86+
You can specify a local file path using the `file://` URI scheme:
87+
88+
```shell
89+
apiclient network configure file:///path/to/net.toml
90+
```
91+
92+
#### Base64 encoded input
93+
94+
For inline configuration, you can provide base64-encoded content:
95+
96+
```shell
97+
apiclient network configure base64:dmVyc2lvbiA9IDIKCltldGgwXQpkaGNwNCA9IHRydWU=
98+
```
99+
100+
This is particularly useful for automation and configuration management where you want to embed the network configuration directly in scripts or user data.
101+
102+
#### Configuration format
103+
104+
The `net.toml` file uses the same format that netdog supports. Here's an example:
105+
106+
```toml
107+
version = 2
108+
109+
[eth0]
110+
dhcp4 = true
111+
dhcp6 = false
112+
113+
[eth1]
114+
static4 = ["192.168.1.100/24"]
115+
route = [{to = "default", via = "192.168.1.1"}]
116+
```
117+
118+
After configuring the network settings, you'll need to reboot the system for the changes to take effect:
119+
120+
```shell
121+
apiclient reboot
122+
```
123+
78124
### Update mode
79125

80126
To start, you can check what updates are available:

sources/api/apiclient/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! The apiclient library provides high-level methods to interact with the Bottlerocket API. See
2-
//! the documentation for submodules [`apply`], [`exec`], [`get`], [`reboot`], [`report`], [`set`],
2+
//! the documentation for submodules [`apply`], [`exec`], [`get`], [`network`], [`reboot`], [`report`], [`set`],
33
//! and [`update`] for high-level helpers.
44
//!
55
//! For more control, and to handle APIs without high-level wrappers, there are also 'raw' methods
@@ -24,6 +24,7 @@ pub mod ephemeral_storage;
2424
pub mod exec;
2525
pub mod get;
2626
pub mod lockdown;
27+
pub mod network;
2728
pub mod reboot;
2829
pub mod report;
2930
pub mod set;

sources/api/apiclient/src/main.rs

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
// to the API, which is intended to be reusable by other crates.
88

99
use apiclient::{
10-
apply, ephemeral_storage, exec, get, lockdown, reboot, report, set, update, SettingsInput,
10+
apply, ephemeral_storage, exec, get, lockdown, network, reboot, report, set, update,
11+
SettingsInput,
1112
};
1213
use log::{info, log_enabled, trace, warn};
1314
use model::ephemeral_storage::{Filesystem, Preference};
@@ -49,6 +50,7 @@ enum Subcommand {
4950
Exec(ExecArgs),
5051
Get(GetArgs),
5152
Lockdown(LockdownArgs),
53+
Network(NetworkSubcommand),
5254
Raw(RawArgs),
5355
Reboot(RebootArgs),
5456
Set(SetArgs),
@@ -185,6 +187,18 @@ struct EphemeralStorageFormatArgs {
185187
format: Option<String>,
186188
}
187189

190+
/// Stores the 'network' subcommand specified by the user.
191+
#[derive(Debug)]
192+
enum NetworkSubcommand {
193+
Configure(NetworkConfigureArgs),
194+
}
195+
196+
/// Stores user-supplied arguments for the 'network configure' subcommand.
197+
#[derive(Debug)]
198+
struct NetworkConfigureArgs {
199+
input_source: Option<String>,
200+
}
201+
188202
/// Informs the user about proper usage of the program and exits.
189203
fn usage() -> ! {
190204
let msg = &format!(
@@ -203,6 +217,8 @@ fn usage() -> ! {
203217
or from stdin.
204218
get Retrieve and print settings.
205219
set Changes settings and applies them to the system.
220+
network configure Configures network settings from net.toml files at
221+
given URIs.
206222
update check Prints information about available updates.
207223
update apply Applies available updates.
208224
update cancel Deactivates an applied update.
@@ -248,6 +264,13 @@ fn usage() -> ! {
248264
If neither prefixes nor URI are specified, get will show
249265
settings and OS info.
250266
267+
network configure options:
268+
[ URI ] URI to a network configuration file (TOML format)
269+
to apply. Supports file:// and base64: URI schemes.
270+
If no URI is specified, reads from stdin.
271+
Configuration is written to /.bottlerocket/net.toml and
272+
validated at next boot by netdog.
273+
251274
set options:
252275
KEY=VALUE [KEY=VALUE ...] The settings you want to set. For example:
253276
settings.motd="hi there" settings.ecs.cluster=example
@@ -376,8 +399,8 @@ fn parse_args(args: impl Iterator<Item = String>) -> (Args, Subcommand) {
376399
}
377400

378401
// Subcommands
379-
"raw" | "apply" | "exec" | "get" | "lockdown" | "reboot" | "report" | "set"
380-
| "update" | "ephemeral-storage"
402+
"raw" | "apply" | "exec" | "get" | "lockdown" | "network" | "reboot" | "report"
403+
| "set" | "update" | "ephemeral-storage"
381404
if subcommand.is_none() && !arg.starts_with('-') =>
382405
{
383406
subcommand = Some(arg)
@@ -395,6 +418,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> (Args, Subcommand) {
395418
Some("exec") => (global_args, parse_exec_args(subcommand_args)),
396419
Some("get") => (global_args, parse_get_args(subcommand_args)),
397420
Some("lockdown") => (global_args, parse_lockdown_args(subcommand_args)),
421+
Some("network") => (global_args, parse_network_args(subcommand_args)),
398422
Some("reboot") => (global_args, parse_reboot_args(subcommand_args)),
399423
Some("report") => (global_args, parse_report_args(subcommand_args)),
400424
Some("set") => (global_args, parse_set_args(subcommand_args)),
@@ -564,6 +588,42 @@ fn parse_lockdown_args(args: Vec<String>) -> Subcommand {
564588
}
565589
Subcommand::Lockdown(LockdownArgs {})
566590
}
591+
/// Parses the desired subcommand of 'network'.
592+
fn parse_network_args(args: Vec<String>) -> Subcommand {
593+
let mut subcommand = None;
594+
let mut subcommand_args = Vec::new();
595+
596+
for arg in args.into_iter() {
597+
match arg.as_ref() {
598+
// Subcommands
599+
"configure" if subcommand.is_none() => subcommand = Some(arg),
600+
601+
// Other arguments are passed to the subcommand parser
602+
_ => subcommand_args.push(arg),
603+
}
604+
}
605+
606+
match subcommand.as_deref() {
607+
Some("configure") => parse_network_configure_args(subcommand_args),
608+
_ => usage_msg("Missing or unknown subcommand for 'network'"),
609+
}
610+
}
611+
612+
/// Parses arguments for the 'network configure' subcommand.
613+
fn parse_network_configure_args(args: Vec<String>) -> Subcommand {
614+
let mut input_source = None;
615+
616+
for arg in args.into_iter() {
617+
if input_source.is_some() {
618+
usage_msg("apiclient network configure takes only one input source URI.")
619+
}
620+
input_source = Some(arg);
621+
}
622+
623+
Subcommand::Network(NetworkSubcommand::Configure(NetworkConfigureArgs {
624+
input_source,
625+
}))
626+
}
567627

568628
/// Parses arguments for the 'reboot' subcommand.
569629
fn parse_reboot_args(args: Vec<String>) -> Subcommand {
@@ -1084,6 +1144,17 @@ async fn run() -> Result<()> {
10841144
.context(error::LockdownSnafu)?;
10851145
}
10861146

1147+
Subcommand::Network(subcommand) => match subcommand {
1148+
NetworkSubcommand::Configure(configure_args) => {
1149+
let content = network::get_content(configure_args.input_source)
1150+
.await
1151+
.context(error::NetworkGetContentSnafu)?;
1152+
network::configure(&args.socket_path, content)
1153+
.await
1154+
.context(error::NetworkConfigureSnafu)?;
1155+
}
1156+
},
1157+
10871158
Subcommand::Reboot(_reboot) => {
10881159
reboot::reboot(&args.socket_path)
10891160
.await
@@ -1252,7 +1323,9 @@ async fn main() {
12521323
}
12531324

12541325
mod error {
1255-
use apiclient::{apply, ephemeral_storage, exec, get, lockdown, reboot, report, set, update};
1326+
use apiclient::{
1327+
apply, ephemeral_storage, exec, get, lockdown, network, reboot, report, set, update,
1328+
};
12561329
use snafu::Snafu;
12571330

12581331
#[derive(Debug, Snafu)]
@@ -1272,6 +1345,11 @@ mod error {
12721345

12731346
#[snafu(display("Failed to lockdown: {}", source))]
12741347
Lockdown { source: lockdown::Error },
1348+
#[snafu(display("Failed to get network configuration content: {}", source))]
1349+
NetworkGetContent { source: network::Error },
1350+
1351+
#[snafu(display("Failed to configure network: {}", source))]
1352+
NetworkConfigure { source: network::Error },
12751353

12761354
#[snafu(display("Failed to reboot: {}", source))]
12771355
Reboot { source: reboot::Error },
@@ -1515,4 +1593,66 @@ mod tests {
15151593
_ => panic!("Expected Get with Prefixes"),
15161594
}
15171595
}
1596+
1597+
#[test]
1598+
fn test_network_configure_parsing_stdin() {
1599+
// Test that network configure with no arguments defaults to stdin
1600+
let (global_args, subcommand) = parse_command_line("apiclient network configure");
1601+
1602+
// Test global arguments match expected defaults
1603+
assert_eq!(global_args.log_level, LevelFilter::Info);
1604+
assert_eq!(global_args.socket_path, "/run/api.sock");
1605+
1606+
// Test network configure subcommand with no input source (stdin)
1607+
match subcommand {
1608+
Subcommand::Network(NetworkSubcommand::Configure(configure_args)) => {
1609+
assert_eq!(configure_args.input_source, None);
1610+
}
1611+
_ => panic!("Expected Network::Configure subcommand, got: {subcommand:?}"),
1612+
}
1613+
}
1614+
1615+
#[test_case("apiclient network configure file:///tmp/net.toml",
1616+
global_args!(LevelFilter::Info),
1617+
"file:///tmp/net.toml";
1618+
"network configure with file URI")]
1619+
#[test_case("apiclient network configure base64:dmVyc2lvbiA9IDIKCltldGgwXQpkaGNwNCA9IHRydWU=",
1620+
global_args!(LevelFilter::Info),
1621+
"base64:dmVyc2lvbiA9IDIKCltldGgwXQpkaGNwNCA9IHRydWU=";
1622+
"network configure with base64 URI")]
1623+
#[test_case("apiclient -v network configure file:///tmp/net.toml",
1624+
global_args!(LevelFilter::Debug),
1625+
"file:///tmp/net.toml";
1626+
"verbose flag with network configure")]
1627+
#[test_case("apiclient --log-level error network configure base64:test",
1628+
global_args!(LevelFilter::Error),
1629+
"base64:test";
1630+
"log level with network configure")]
1631+
#[test_case("apiclient --socket-path /tmp/custom.sock network configure file:///etc/net.toml",
1632+
global_args!(LevelFilter::Info, "/tmp/custom.sock"),
1633+
"file:///etc/net.toml";
1634+
"custom socket path with network configure")]
1635+
fn test_network_configure_parsing(
1636+
cmd_str: &str,
1637+
expected_args: Args,
1638+
expected_input_source: &str,
1639+
) {
1640+
// Given a command line string for network configure
1641+
let (global_args, subcommand) = parse_command_line(cmd_str);
1642+
1643+
// Test global arguments match expected values
1644+
assert_eq!(global_args.log_level, expected_args.log_level);
1645+
assert_eq!(global_args.socket_path, expected_args.socket_path);
1646+
1647+
// Test network configure subcommand and extract input source
1648+
match subcommand {
1649+
Subcommand::Network(NetworkSubcommand::Configure(configure_args)) => {
1650+
assert_eq!(
1651+
configure_args.input_source,
1652+
Some(expected_input_source.to_string())
1653+
);
1654+
}
1655+
_ => panic!("Expected Network::Configure subcommand, got: {subcommand:?}"),
1656+
}
1657+
}
15181658
}

0 commit comments

Comments
 (0)