libcoreinst/io/
bls.rs

1// Copyright 2021 CoreOS, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities for reading/writing BLS configs, including kernel arguments.
16
17use anyhow::{bail, Context, Result};
18use lazy_static::lazy_static;
19use regex::Regex;
20use std::fs::read_dir;
21use std::path::{Path, PathBuf};
22
23/// Calls a function on the latest (default) BLS entry and optionally updates it if the function
24/// returns new content. Errors out if no BLS entry was found.
25///
26/// Note that on s390x, this does not handle the call to `zipl`. We expect it to be done at a
27/// higher level if needed for batching purposes.
28///
29/// Returns `true` if BLS content was modified.
30pub fn visit_bls_entry(
31    mountpoint: &Path,
32    mut f: impl FnMut(&str) -> Result<Option<String>>,
33) -> Result<bool> {
34    // walk /boot/loader/entries/*.conf
35    let mut config_path = mountpoint.to_path_buf();
36    config_path.push("loader/entries");
37
38    // We only want to affect the latest BLS entry (i.e. the default one). This confusingly is the
39    // *last* BLS config in the directory because they are sorted by reverse order:
40    // https://github.com/ostreedev/ostree/pull/1654
41    //
42    // Because `read_dir` doesn't guarantee any ordering, we gather all the filenames up front and
43    // sort them before picking the last one.
44    let mut entries: Vec<PathBuf> = Vec::new();
45    for entry in read_dir(&config_path)
46        .with_context(|| format!("reading directory {}", config_path.display()))?
47    {
48        let path = entry
49            .with_context(|| format!("reading directory {}", config_path.display()))?
50            .path();
51        if path.extension().unwrap_or_default() != "conf" {
52            continue;
53        }
54        entries.push(path);
55    }
56    entries.sort();
57
58    let mut changed = false;
59    if let Some(path) = entries.pop() {
60        let orig_contents = std::fs::read_to_string(&path)
61            .with_context(|| format!("reading {}", path.display()))?;
62        let r = f(&orig_contents).with_context(|| format!("visiting {}", path.display()))?;
63
64        if let Some(new_contents) = r {
65            // write out the modified data
66            std::fs::write(&path, new_contents.as_bytes())
67                .with_context(|| format!("writing {}", path.display()))?;
68            changed = true;
69        }
70    } else {
71        bail!("Found no BLS entries in {}", config_path.display());
72    }
73
74    Ok(changed)
75}
76
77/// Wrapper around `visit_bls_entry` to specifically visit just the BLS entry's `options` line and
78/// optionally update it if the function returns new content. Errors out if none or more than one
79/// `options` field was found. Returns `true` if BLS content was modified.
80pub fn visit_bls_entry_options(
81    mountpoint: &Path,
82    f: impl Fn(&str) -> Result<Option<String>>,
83) -> Result<bool> {
84    visit_bls_entry(mountpoint, |orig_contents: &str| {
85        let mut new_contents = String::with_capacity(orig_contents.len());
86        let mut found_options = false;
87        let mut modified = false;
88        for line in orig_contents.lines() {
89            if !line.starts_with("options ") {
90                new_contents.push_str(line.trim_end());
91            } else if found_options {
92                bail!("Multiple 'options' lines found");
93            } else {
94                let r = f(line["options ".len()..].trim()).context("visiting options")?;
95                if let Some(new_options) = r {
96                    new_contents.push_str("options ");
97                    new_contents.push_str(new_options.trim());
98                    modified = true;
99                }
100                found_options = true;
101            }
102            new_contents.push('\n');
103        }
104        if !found_options {
105            bail!("Couldn't locate 'options' line");
106        }
107        if !modified {
108            Ok(None)
109        } else {
110            Ok(Some(new_contents))
111        }
112    })
113}
114
115#[derive(Default, PartialEq, Eq)]
116pub struct KargsEditor {
117    append: Vec<String>,
118    append_if_missing: Vec<String>,
119    replace: Vec<String>,
120    delete: Vec<String>,
121}
122
123impl KargsEditor {
124    pub fn new() -> Self {
125        Default::default()
126    }
127
128    pub fn append(&mut self, args: &[String]) -> &mut Self {
129        self.append.extend_from_slice(args);
130        self
131    }
132
133    pub fn append_if_missing(&mut self, args: &[String]) -> &mut Self {
134        self.append_if_missing.extend_from_slice(args);
135        self
136    }
137
138    pub fn replace(&mut self, args: &[String]) -> &mut Self {
139        self.replace.extend_from_slice(args);
140        self
141    }
142
143    pub fn delete(&mut self, args: &[String]) -> &mut Self {
144        self.delete.extend_from_slice(args);
145        self
146    }
147
148    // XXX: Need a proper parser here and share it with afterburn. The approach we use here
149    // is to just do a dumb substring search and replace. This is naive (e.g. doesn't
150    // handle occurrences in quoted args) but will work for now (one thing that saves us is
151    // that we're acting on our baked configs, which have straight-forward kargs).
152    pub fn apply_to(&self, current_kargs: &str) -> Result<String> {
153        lazy_static! {
154            static ref RE: Regex = Regex::new(r"^([^=]+)=([^=]+)=([^=]+)$").unwrap();
155        }
156        let mut new_kargs: String = format!(" {current_kargs} ");
157        for karg in &self.delete {
158            let s = format!(" {} ", karg.trim());
159            new_kargs = new_kargs.replace(&s, " ");
160        }
161        for karg in &self.append {
162            new_kargs.push_str(karg.trim());
163            new_kargs.push(' ');
164        }
165        for karg in &self.append_if_missing {
166            let karg = karg.trim();
167            let s = format!(" {karg} ");
168            if !new_kargs.contains(&s) {
169                new_kargs.push_str(karg);
170                new_kargs.push(' ');
171            }
172        }
173        for karg in &self.replace {
174            let caps = match RE.captures(karg) {
175                Some(caps) => caps,
176                None => bail!("Wrong input, format should be: KEY=OLD=NEW"),
177            };
178            let old = format!(" {}={} ", &caps[1], &caps[2]);
179            let new = format!(" {}={} ", &caps[1], &caps[3]);
180            new_kargs = new_kargs.replace(&old, &new);
181        }
182        Ok(new_kargs.trim().into())
183    }
184
185    /// Return None if we haven't been asked to do anything, otherwise
186    /// Some(modified args).
187    /// To be used with `visit_bls_entry_options()`.
188    pub fn maybe_apply_to(&self, current_kargs: &str) -> Result<Option<String>> {
189        if self == &Self::new() {
190            Ok(None)
191        } else {
192            Ok(Some(self.apply_to(current_kargs)?))
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_apply_to() {
203        let orig_kargs = "foo bar foobar";
204
205        let delete_kargs = vec!["foo".into()];
206        let new_kargs = KargsEditor::new()
207            .delete(&delete_kargs)
208            .apply_to(orig_kargs)
209            .unwrap();
210        assert_eq!(new_kargs, "bar foobar");
211
212        let delete_kargs = vec!["bar".into()];
213        let new_kargs = KargsEditor::new()
214            .delete(&delete_kargs)
215            .apply_to(orig_kargs)
216            .unwrap();
217        assert_eq!(new_kargs, "foo foobar");
218
219        let delete_kargs = vec!["foobar".into()];
220        let new_kargs = KargsEditor::new()
221            .delete(&delete_kargs)
222            .apply_to(orig_kargs)
223            .unwrap();
224        assert_eq!(new_kargs, "foo bar");
225
226        let delete_kargs = vec!["foo bar".into()];
227        let new_kargs = KargsEditor::new()
228            .delete(&delete_kargs)
229            .apply_to(orig_kargs)
230            .unwrap();
231        assert_eq!(new_kargs, "foobar");
232
233        let delete_kargs = vec!["bar".into(), "foo".into()];
234        let new_kargs = KargsEditor::new()
235            .delete(&delete_kargs)
236            .apply_to(orig_kargs)
237            .unwrap();
238        assert_eq!(new_kargs, "foobar");
239
240        let orig_kargs = "foo=val bar baz=val";
241
242        let delete_kargs = vec!["   foo=val".into()];
243        let new_kargs = KargsEditor::new()
244            .delete(&delete_kargs)
245            .apply_to(orig_kargs)
246            .unwrap();
247        assert_eq!(new_kargs, "bar baz=val");
248
249        let delete_kargs = vec!["baz=val  ".into()];
250        let new_kargs = KargsEditor::new()
251            .delete(&delete_kargs)
252            .apply_to(orig_kargs)
253            .unwrap();
254        assert_eq!(new_kargs, "foo=val bar");
255
256        let orig_kargs = "foo mitigations=auto,nosmt console=tty0 bar console=ttyS0,115200n8 baz";
257
258        let delete_kargs = vec![
259            "mitigations=auto,nosmt".into(),
260            "console=ttyS0,115200n8".into(),
261        ];
262        let append_kargs = vec!["console=ttyS1,115200n8  ".into()];
263        let append_kargs_if_missing =
264                 // base       // append_kargs dupe             // missing
265            vec!["bar".into(), "console=ttyS1,115200n8".into(), "boo".into()];
266        let new_kargs = KargsEditor::new()
267            .delete(&delete_kargs)
268            .append(&append_kargs)
269            .append_if_missing(&append_kargs_if_missing)
270            .apply_to(orig_kargs)
271            .unwrap();
272        assert_eq!(
273            new_kargs,
274            "foo console=tty0 bar baz console=ttyS1,115200n8 boo"
275        );
276
277        let orig_kargs = "foo mitigations=auto,nosmt console=tty0 bar console=ttyS0,115200n8 baz";
278
279        let append_kargs = vec!["console=ttyS1,115200n8".into()];
280        let replace_kargs = vec!["mitigations=auto,nosmt=auto".into()];
281        let delete_kargs = vec!["console=ttyS0,115200n8".into()];
282        let new_kargs = KargsEditor::new()
283            .append(&append_kargs)
284            .replace(&replace_kargs)
285            .delete(&delete_kargs)
286            .apply_to(orig_kargs)
287            .unwrap();
288        assert_eq!(
289            new_kargs,
290            "foo mitigations=auto console=tty0 bar baz console=ttyS1,115200n8"
291        );
292    }
293
294    #[test]
295    fn test_maybe_apply_to() {
296        // no arguments
297        assert!(KargsEditor::new()
298            .maybe_apply_to("foo bar foobar")
299            .unwrap()
300            .is_none());
301
302        // empty arguments
303        assert!(KargsEditor::new()
304            .append(&[])
305            .delete(&[])
306            .maybe_apply_to("foo bar foobar")
307            .unwrap()
308            .is_none());
309
310        // arguments that aren't relevant
311        let new_kargs = KargsEditor::new()
312            .delete(&["baz".into()])
313            .maybe_apply_to("foo bar foobar")
314            .unwrap()
315            .unwrap();
316        assert_eq!(new_kargs, "foo bar foobar");
317    }
318}