Skip to content

Commit 4199f8b

Browse files
committed
Implement a single Provision interface
This provides a unified interface to provision the host with the option to select the tool used for setting the hostname, creating the user, and so on. By default, the library will try all the provisioning methods it knows of until one succeeds. Users of the library can optionally specify a subset to attempt when provisioning. This allows users to decide which tool or tools to use when provisioning. Some feature flags have been added to `azure-init` which enable provisioning with a tool, letting you build binaries for a particular platform relatively easily.
1 parent 773f4cf commit 4199f8b

File tree

12 files changed

+488
-64
lines changed

12 files changed

+488
-64
lines changed

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ path = "tests/functional_tests.rs"
3232
members = [
3333
"libazureinit",
3434
]
35+
36+
[features]
37+
passwd = []
38+
hostnamectl = []
39+
useradd = []
40+
41+
systemd_linux = ["passwd", "hostnamectl", "useradd"]
42+
43+
default = ["systemd_linux"]

libazureinit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ serde_json = "1.0.96"
1818
nix = {version = "0.29.0", features = ["fs", "user"]}
1919
block-utils = "0.11.1"
2020
tracing = "0.1.40"
21+
strum = { version = "0.26.3", features = ["derive"] }
2122

2223
[dev-dependencies]
2324
tempfile = "3"

libazureinit/src/error.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,16 @@ pub enum Error {
3131
NonEmptyPassword,
3232
#[error("Unable to get list of block devices")]
3333
BlockUtils(#[from] block_utils::BlockUtilsError),
34+
#[error(
35+
"Failed to set the hostname; none of the provided backends succeeded"
36+
)]
37+
NoHostnameProvisioner,
38+
#[error(
39+
"Failed to create a user; none of the provided backends succeeded"
40+
)]
41+
NoUserProvisioner,
42+
#[error(
43+
"Failed to set the user password; none of the provided backends succeeded"
44+
)]
45+
NoPasswordProvisioner,
3446
}

libazureinit/src/imds.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ pub struct PublicKeys {
5757
pub path: String,
5858
}
5959

60+
impl From<&str> for PublicKeys {
61+
fn from(value: &str) -> Self {
62+
Self {
63+
key_data: value.to_string(),
64+
path: String::new(),
65+
}
66+
}
67+
}
68+
6069
/// Deserializer that handles the string "true" and "false" that the IMDS API returns.
6170
fn string_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
6271
where

libazureinit/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
pub mod distro;
54
pub mod error;
65
pub mod goalstate;
76
pub mod imds;
87
pub mod media;
9-
pub mod user;
8+
9+
mod provision;
10+
pub use provision::{
11+
hostname::Provisioner as HostnameProvisioner,
12+
password::Provisioner as PasswordProvisioner,
13+
user::{Provisioner as UserProvisioner, User},
14+
Provision,
15+
};
1016

1117
// Re-export as the Client is used in our API.
1218
pub use reqwest;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::process::Command;
5+
6+
use tracing::instrument;
7+
8+
use crate::error::Error;
9+
10+
/// Available tools to set the host's hostname.
11+
#[derive(strum::EnumIter, Debug, Clone)]
12+
#[non_exhaustive]
13+
pub enum Provisioner {
14+
/// Use the `hostnamectl` command from `systemd`.
15+
Hostnamectl,
16+
#[cfg(test)]
17+
FakeHostnamectl,
18+
}
19+
20+
impl Provisioner {
21+
pub(crate) fn set(&self, hostname: impl AsRef<str>) -> Result<(), Error> {
22+
match self {
23+
Self::Hostnamectl => hostnamectl(hostname.as_ref()),
24+
#[cfg(test)]
25+
Self::FakeHostnamectl => Ok(()),
26+
}
27+
}
28+
}
29+
30+
#[instrument(skip_all)]
31+
fn hostnamectl(hostname: &str) -> Result<(), Error> {
32+
let path_hostnamectl = env!("PATH_HOSTNAMECTL");
33+
34+
let status = Command::new(path_hostnamectl)
35+
.arg("set-hostname")
36+
.arg(hostname)
37+
.status()?;
38+
if status.success() {
39+
Ok(())
40+
} else {
41+
Err(Error::SubprocessFailed {
42+
command: path_hostnamectl.to_string(),
43+
status,
44+
})
45+
}
46+
}

libazureinit/src/provision/mod.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
pub mod hostname;
4+
pub mod password;
5+
pub(crate) mod ssh;
6+
pub mod user;
7+
8+
use strum::IntoEnumIterator;
9+
use tracing::instrument;
10+
11+
use crate::error::Error;
12+
use crate::User;
13+
14+
/// The interface for applying the desired configuration to the host.
15+
///
16+
/// By default, all known tools for provisioning a particular resource are tried
17+
/// until one succeeds. Particular tools can be selected via the
18+
/// `*_provisioners()` methods ([`Provision::hostname_provisioners`],
19+
/// [`Provision::user_provisioners`], etc).
20+
///
21+
/// To actually apply the configuration, use [`Provision::provision`].
22+
#[derive(Clone)]
23+
pub struct Provision {
24+
hostname: String,
25+
user: User,
26+
hostname_backends: Option<Vec<hostname::Provisioner>>,
27+
user_backends: Option<Vec<user::Provisioner>>,
28+
password_backends: Option<Vec<password::Provisioner>>,
29+
}
30+
31+
impl Provision {
32+
pub fn new(hostname: impl Into<String>, user: User) -> Self {
33+
Self {
34+
hostname: hostname.into(),
35+
user,
36+
hostname_backends: None,
37+
user_backends: None,
38+
password_backends: None,
39+
}
40+
}
41+
42+
/// Specify the ways to set the virtual machine's hostname.
43+
///
44+
/// By default, all known methods will be attempted. Use this function to
45+
/// restrict which methods are attempted. These will be attempted in the
46+
/// order provided until one succeeds.
47+
pub fn hostname_provisioners(
48+
mut self,
49+
backends: impl Into<Vec<hostname::Provisioner>>,
50+
) -> Self {
51+
self.hostname_backends = Some(backends.into());
52+
self
53+
}
54+
55+
/// Specify the ways to create a user in the virtual machine
56+
///
57+
/// By default, all known methods will be attempted. Use this function to
58+
/// restrict which methods are attempted. These will be attempted in the
59+
/// order provided until one succeeds.
60+
pub fn user_provisioners(
61+
mut self,
62+
backends: impl Into<Vec<user::Provisioner>>,
63+
) -> Self {
64+
self.user_backends = Some(backends.into());
65+
self
66+
}
67+
68+
/// Specify the ways to set a users password.
69+
///
70+
/// By default, all known methods will be attempted. Use this function to
71+
/// restrict which methods are attempted. These will be attempted in the
72+
/// order provided until one succeeds. Only relevant if a password has been
73+
/// provided with the [`User`].
74+
pub fn password_provisioners(
75+
mut self,
76+
backend: impl Into<Vec<password::Provisioner>>,
77+
) -> Self {
78+
self.password_backends = Some(backend.into());
79+
self
80+
}
81+
82+
/// Provision the host.
83+
///
84+
/// Provisioning can fail if the host lacks the necessary tools. For example,
85+
/// if there is no `useradd` command on the system's `PATH`, or if the command
86+
/// returns an error, this will return an error. It does not attempt to undo
87+
/// partial provisioning.
88+
#[instrument(skip_all)]
89+
pub fn provision(self) -> Result<(), Error> {
90+
self.user_backends
91+
.unwrap_or_else(|| user::Provisioner::iter().collect())
92+
.iter()
93+
.find_map(|backend| {
94+
backend
95+
.create(&self.user)
96+
.map_err(|e| {
97+
tracing::info!(
98+
error=?e,
99+
backend=?backend,
100+
resource="user",
101+
"Provisioning did not succeed"
102+
);
103+
e
104+
})
105+
.ok()
106+
})
107+
.ok_or(Error::NoUserProvisioner)?;
108+
109+
self.password_backends
110+
.unwrap_or_else(|| password::Provisioner::iter().collect())
111+
.iter()
112+
.find_map(|backend| {
113+
backend
114+
.set(&self.user)
115+
.map_err(|e| {
116+
tracing::info!(
117+
error=?e,
118+
backend=?backend,
119+
resource="password",
120+
"Provisioning did not succeed"
121+
);
122+
e
123+
})
124+
.ok()
125+
})
126+
.ok_or(Error::NoPasswordProvisioner)?;
127+
128+
if !self.user.ssh_keys.is_empty() {
129+
let user = nix::unistd::User::from_name(&self.user.name)?.ok_or(
130+
Error::UserMissing {
131+
user: self.user.name,
132+
},
133+
)?;
134+
ssh::provision_ssh(&user, &self.user.ssh_keys)?;
135+
}
136+
137+
self.hostname_backends
138+
.unwrap_or_else(|| hostname::Provisioner::iter().collect())
139+
.iter()
140+
.find_map(|backend| {
141+
backend
142+
.set(&self.hostname)
143+
.map_err(|e| {
144+
tracing::info!(
145+
error=?e,
146+
backend=?backend,
147+
resource="hostname",
148+
"Provisioning did not succeed"
149+
);
150+
e
151+
})
152+
.ok()
153+
})
154+
.ok_or(Error::NoHostnameProvisioner)?;
155+
156+
Ok(())
157+
}
158+
}
159+
160+
#[cfg(test)]
161+
mod tests {
162+
163+
use crate::User;
164+
165+
use super::{hostname, password, user, Provision};
166+
167+
#[test]
168+
fn test_successful_provision() {
169+
let _p = Provision::new(
170+
"my-hostname".to_string(),
171+
User::new("azureuser", vec![]),
172+
)
173+
.hostname_provisioners([hostname::Provisioner::FakeHostnamectl])
174+
.user_provisioners([user::Provisioner::FakeUseradd])
175+
.password_provisioners([password::Provisioner::FakePasswd])
176+
.provision()
177+
.unwrap();
178+
}
179+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::process::Command;
5+
6+
use tracing::instrument;
7+
8+
use crate::{error::Error, User};
9+
10+
/// Available tools to set the user's password (if a password is provided).
11+
#[derive(strum::EnumIter, Debug, Clone)]
12+
#[non_exhaustive]
13+
pub enum Provisioner {
14+
/// Use the `passwd` command from `shadow-utils`.
15+
Passwd,
16+
#[cfg(test)]
17+
FakePasswd,
18+
}
19+
20+
impl Provisioner {
21+
pub(crate) fn set(&self, user: &User) -> Result<(), Error> {
22+
match self {
23+
Self::Passwd => passwd(user),
24+
#[cfg(test)]
25+
Self::FakePasswd => Ok(()),
26+
}
27+
}
28+
}
29+
30+
#[instrument(skip_all)]
31+
fn passwd(user: &User) -> Result<(), Error> {
32+
let path_passwd = env!("PATH_PASSWD");
33+
34+
if user.password.is_none() {
35+
let status = Command::new(path_passwd)
36+
.arg("-d")
37+
.arg(&user.name)
38+
.status()?;
39+
if !status.success() {
40+
return Err(Error::SubprocessFailed {
41+
command: path_passwd.to_string(),
42+
status,
43+
});
44+
}
45+
} else {
46+
// creating user with a non-empty password is not allowed.
47+
return Err(Error::NonEmptyPassword);
48+
}
49+
50+
Ok(())
51+
}

libazureinit/src/user.rs renamed to libazureinit/src/provision/ssh.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,6 @@ use tracing::instrument;
1212
use crate::error::Error;
1313
use crate::imds::PublicKeys;
1414

15-
pub fn set_ssh_keys(
16-
keys: Vec<PublicKeys>,
17-
username: impl AsRef<str>,
18-
) -> Result<(), Error> {
19-
let user =
20-
nix::unistd::User::from_name(username.as_ref())?.ok_or_else(|| {
21-
Error::UserMissing {
22-
user: username.as_ref().to_string(),
23-
}
24-
})?;
25-
provision_ssh(&user, &keys)
26-
}
27-
2815
#[instrument(skip_all, name = "ssh")]
2916
pub(crate) fn provision_ssh(
3017
user: &nix::unistd::User,

0 commit comments

Comments
 (0)