service: Mark .applied with checksum
At some envs like openshift machine-config-operator moving configuration files can break in place upgrades. This chane use checksums storead at the .applied files to check for changes. Signed-off-by: Enrique Llorente <ellorent@redhat.com>
This commit is contained in:
parent
dbd6eca4b6
commit
dbb98e1d30
@ -9,8 +9,8 @@ nmstate\&.service
|
||||
nmstate\&.service invokes \fBnmstatectl service\fR command which
|
||||
apply all network state files ending with \fB.yml\fR in
|
||||
\fB/etc/nmstate\fR folder.
|
||||
The applied network state file will be renamed with postfix \fB.applied\fR
|
||||
to prevent repeated applied on next service start.
|
||||
The applied network state file sha256 digest will be stored at \fB.applied\fR
|
||||
file to prevent repeated applied on next service start.
|
||||
.SH BUG REPORTS
|
||||
Report bugs on nmstate GitHub issues <https://github.com/nmstate/nmstate>.
|
||||
.SH COPYRIGHT
|
||||
|
@ -28,6 +28,7 @@ ctrlc = { version = "3.2.1", optional = true }
|
||||
uuid = { version = "1.1", features = ["v4"] }
|
||||
chrono = "0.4"
|
||||
nispor = { version = "1.2", optional = true }
|
||||
ring = "0.17.7"
|
||||
|
||||
[features]
|
||||
default = ["query_apply", "gen_conf", "gen_revert"]
|
||||
|
@ -1,12 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{apply::apply, error::CliError};
|
||||
|
||||
use ring::digest::{Context, SHA256};
|
||||
|
||||
const CONFIG_FILE_EXTENTION: &str = "yml";
|
||||
const RELOCATE_FILE_EXTENTION: &str = "applied";
|
||||
const CHECKSUM_FILE_EXTENTION: &str = "applied";
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Clone, PartialOrd, Ord)]
|
||||
struct FileChecksum {
|
||||
path: PathBuf,
|
||||
checksum: String,
|
||||
}
|
||||
|
||||
impl FileChecksum {
|
||||
fn new(path: PathBuf, checksum: String) -> Self {
|
||||
Self { path, checksum }
|
||||
}
|
||||
}
|
||||
|
||||
struct HexSlice<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> fmt::Display for HexSlice<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for &byte in self.0 {
|
||||
write!(f, "{:0>2x}", byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ncl_service(
|
||||
matches: &clap::ArgMatches,
|
||||
@ -27,7 +57,7 @@ pub(crate) fn ncl_service(
|
||||
};
|
||||
if config_files.is_empty() {
|
||||
log::info!(
|
||||
"No nmstate config(end with .{}) found in config folder {}",
|
||||
"No new nmstate config(end with .{}) found in config folder {}",
|
||||
CONFIG_FILE_EXTENTION,
|
||||
folder
|
||||
);
|
||||
@ -39,24 +69,29 @@ pub(crate) fn ncl_service(
|
||||
// We sleep for 2 seconds here to avoid meaningless retry.
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
for file_path in config_files {
|
||||
let mut fd = match std::fs::File::open(&file_path) {
|
||||
for config_file in config_files {
|
||||
let mut fd = match std::fs::File::open(&config_file.path) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to read config file {}: {e}",
|
||||
file_path.display()
|
||||
config_file.path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match apply(&mut fd, matches) {
|
||||
Ok(_) => {
|
||||
log::info!("Applied nmstate config: {}", file_path.display());
|
||||
if let Err(e) = relocate_file(&file_path) {
|
||||
log::info!(
|
||||
"Applied nmstate config: {}",
|
||||
config_file.path.display()
|
||||
);
|
||||
if let Err(e) =
|
||||
write_checksum(&config_file.path, &config_file.checksum)
|
||||
{
|
||||
log::error!(
|
||||
"Failed to rename applied state file: {} {}",
|
||||
file_path.display(),
|
||||
"Failed to generate checksum file: {} {}",
|
||||
config_file.path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
@ -64,7 +99,7 @@ pub(crate) fn ncl_service(
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to apply state file {}: {}",
|
||||
file_path.display(),
|
||||
config_file.path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
@ -74,28 +109,72 @@ pub(crate) fn ncl_service(
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
// All file ending with `.yml` will be included.
|
||||
fn get_config_files(folder: &str) -> Result<Vec<PathBuf>, CliError> {
|
||||
// All file ending with `.yml` that do not have a sha256 checksum stored at
|
||||
// a `.applied` file or the checksum stored differs.
|
||||
fn get_config_files(folder: &str) -> Result<Vec<FileChecksum>, CliError> {
|
||||
let folder = Path::new(folder);
|
||||
let mut ret = Vec::new();
|
||||
let mut yml_files = HashSet::<FileChecksum>::new();
|
||||
let mut applied_files = HashSet::<FileChecksum>::new();
|
||||
for entry in folder.read_dir()? {
|
||||
let file = entry?.path();
|
||||
if file.extension() == Some(OsStr::new(CONFIG_FILE_EXTENTION)) {
|
||||
ret.push(folder.join(file));
|
||||
let digest = sha256_digest(&file)?;
|
||||
yml_files.insert(FileChecksum::new(
|
||||
folder.join(file).with_extension(""),
|
||||
digest,
|
||||
));
|
||||
} else if file.extension() == Some(OsStr::new(CHECKSUM_FILE_EXTENTION))
|
||||
{
|
||||
let digest = fs::read_to_string(&file)?;
|
||||
applied_files.insert(FileChecksum::new(
|
||||
folder.join(file).with_extension(""),
|
||||
digest,
|
||||
));
|
||||
}
|
||||
}
|
||||
ret.sort_unstable();
|
||||
let mut ret: Vec<_> = yml_files
|
||||
.difference(&applied_files)
|
||||
.cloned()
|
||||
.map(|f| {
|
||||
FileChecksum::new(
|
||||
f.path.with_extension(CONFIG_FILE_EXTENTION),
|
||||
f.checksum,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
ret.sort_by_key(|f| f.path.clone());
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
// rename file by adding a suffix `.applied`.
|
||||
pub(crate) fn relocate_file(file_path: &Path) -> Result<(), CliError> {
|
||||
let new_path = file_path.with_extension(RELOCATE_FILE_EXTENTION);
|
||||
std::fs::rename(file_path, &new_path)?;
|
||||
// Dump state checksum to `.applied` file.
|
||||
pub(crate) fn write_checksum(
|
||||
file_path: &Path,
|
||||
checksum: &str,
|
||||
) -> Result<(), CliError> {
|
||||
let checksum_file_path = file_path.with_extension(CHECKSUM_FILE_EXTENTION);
|
||||
fs::write(&checksum_file_path, checksum)?;
|
||||
log::info!(
|
||||
"Renamed applied config {} to {}",
|
||||
"Checksum for config {} stored at {}",
|
||||
file_path.display(),
|
||||
new_path.display()
|
||||
checksum_file_path.display(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sha256_digest(file_path: &PathBuf) -> Result<String, CliError> {
|
||||
let mut context = Context::new(&SHA256);
|
||||
let mut buffer = [0; 1024];
|
||||
|
||||
let input = File::open(file_path)?;
|
||||
let mut reader = BufReader::new(input);
|
||||
|
||||
loop {
|
||||
let count = reader.read(&mut buffer)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
context.update(&buffer[..count]);
|
||||
}
|
||||
|
||||
Ok(format!("{}", HexSlice(context.finish().as_ref())))
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
import pytest
|
||||
@ -73,6 +75,13 @@ TEST_CONFIG3_APPLIED_FILE_PATH = f"{CONFIG_DIR}/03-nmstate-policy-test.applied"
|
||||
DUMMY1 = "dummy1"
|
||||
|
||||
|
||||
def sha256sum(filename):
|
||||
with open(filename, "rb", buffering=0) as f:
|
||||
digest = hashlib.sha256()
|
||||
digest.update(f.read())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nmstate_etc_config():
|
||||
if not os.path.isdir(CONFIG_DIR):
|
||||
@ -103,6 +112,8 @@ def nmstate_etc_config():
|
||||
)
|
||||
os.remove(TEST_CONFIG1_APPLIED_FILE_PATH)
|
||||
os.remove(TEST_CONFIG2_APPLIED_FILE_PATH)
|
||||
os.remove(TEST_CONFIG1_FILE_PATH)
|
||||
os.remove(TEST_CONFIG2_FILE_PATH)
|
||||
|
||||
|
||||
def test_nmstate_service_apply(nmstate_etc_config):
|
||||
@ -111,10 +122,14 @@ def test_nmstate_service_apply(nmstate_etc_config):
|
||||
desire_state = yaml.load(TEST_YAML2_CONTENT, Loader=yaml.SafeLoader)
|
||||
assert_state_match(desire_state)
|
||||
|
||||
assert not os.path.exists(TEST_CONFIG1_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG1_APPLIED_FILE_PATH)
|
||||
assert not os.path.exists(TEST_CONFIG2_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG2_APPLIED_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG1_FILE_PATH)
|
||||
assert Path(TEST_CONFIG1_APPLIED_FILE_PATH).read_text() == sha256sum(
|
||||
TEST_CONFIG1_FILE_PATH
|
||||
)
|
||||
assert os.path.isfile(TEST_CONFIG2_FILE_PATH)
|
||||
assert Path(TEST_CONFIG2_APPLIED_FILE_PATH).read_text() == sha256sum(
|
||||
TEST_CONFIG2_FILE_PATH
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -153,9 +168,13 @@ def test_nmstate_service_apply_nmpolicy(dummy1_up):
|
||||
try:
|
||||
exec_cmd("systemctl restart nmstate".split(), check=True)
|
||||
assert_absent(DUMMY1)
|
||||
assert os.path.isfile(TEST_CONFIG3_APPLIED_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG3_FILE_PATH)
|
||||
assert Path(TEST_CONFIG3_APPLIED_FILE_PATH).read_text() == sha256sum(
|
||||
TEST_CONFIG3_FILE_PATH
|
||||
)
|
||||
finally:
|
||||
os.remove(TEST_CONFIG3_APPLIED_FILE_PATH)
|
||||
os.remove(TEST_CONFIG3_FILE_PATH)
|
||||
|
||||
|
||||
def test_nmstate_service_without_etc_folder():
|
||||
|
Loading…
x
Reference in New Issue
Block a user