bpf_loader_lib/meta/
arg_builder.rs

1//!  SPDX-License-Identifier: MIT
2//!
3//! Copyright (c) 2023, eunomia-bpf
4//! All rights reserved.
5//!
6
7use anyhow::{bail, Result};
8use clap::{Arg, ArgAction, Command};
9use serde_json::Value;
10
11use super::EunomiaObjectMeta;
12
13const DEFAULT_DESCRIPTION: &str = "A simple eBPF program";
14const DEFAULT_VERSION: &str = "0.1.0";
15const DEFAULT_EPILOG: &str = "Built with eunomia-bpf framework.\nSee https://github.com/eunomia-bpf/eunomia-bpf for more information.";
16
17impl EunomiaObjectMeta {
18    /// Build an argument parser use the `cmdarg` sections in .rodata/.bss variables.
19    ///
20    /// Each variable in the `.bss` or `.rodata` sections will be mapped into a command line argument.
21    ///
22    /// If a variable has it's default value, the default value will be used in the command line parser.
23    ///
24    /// Variables with `bool` will have some special cases:
25    /// - If the variable has no default values, a switch named `--<NAME>` will be added, indicating to set the value to true or false
26    /// - If the default value if true, a switch named `--disable-<NAME>` will be added, means set the value to false
27    /// - If the default value if false, a switch named `--enable-<NAME>` will be added, means to set the value to true.
28    ///
29    /// The first will be used to set the value of the variable to `true`, second one will be used to set `false`
30    ///
31    /// Variables with other types will accept values. But values will be checked in `parse_arguments_and_fill_skeleton_variables`, so here the values input in the command line parser will be regarded as strings.
32    pub fn build_argument_parser(&self) -> Result<Command> {
33        let cmd = Command::new(self.bpf_skel.obj_name.to_string());
34
35        let cmd = if let Some(doc) = &self.bpf_skel.doc {
36            cmd.version(
37                doc.version
38                    .to_owned()
39                    .unwrap_or_else(|| DEFAULT_VERSION.to_string()),
40            )
41            .after_help(
42                doc.details
43                    .to_owned()
44                    .unwrap_or_else(|| DEFAULT_EPILOG.to_owned()),
45            )
46            .before_help(
47                doc.brief
48                    .to_owned()
49                    .or(doc.description.to_owned())
50                    .unwrap_or_else(|| DEFAULT_DESCRIPTION.to_owned()),
51            )
52        } else {
53            cmd.version(DEFAULT_VERSION)
54                .after_help(DEFAULT_EPILOG)
55                .before_help(DEFAULT_DESCRIPTION)
56        };
57        // Add a switch to control whether to show debug information
58        let mut cmd = cmd.arg(
59            Arg::new("verbose")
60                .long("verbose")
61                .action(ArgAction::SetTrue)
62                .help("Whether to show libbpf debug information"),
63        );
64        // Add arguments for section vars
65        for section in self.bpf_skel.data_sections.iter() {
66            for variable in section.variables.iter() {
67                // Ignore useless variables
68                if variable.name.starts_with("__eunomia_dummy") {
69                    continue;
70                }
71                let help = variable
72                    .cmdarg
73                    .help
74                    .to_owned()
75                    .or(variable.description.to_owned())
76                    .unwrap_or_else(|| {
77                        format!("Set value of `{}` variable {}", variable.ty, variable.name)
78                    });
79
80                let long = variable
81                    .cmdarg
82                    .long
83                    .to_owned()
84                    .unwrap_or_else(|| variable.name.to_string());
85                if variable.ty == "bool" {
86                    // If there is default values
87                    let default = if let Some(val) = variable
88                        .cmdarg
89                        .default
90                        .to_owned()
91                        .or(variable.value.to_owned())
92                    {
93                        Some(match val {
94                            Value::Bool(b) => b,
95                            _ => bail!("Only expected bool values in bool variables"),
96                        })
97                    } else {
98                        None
99                    };
100                    let arg = match default {
101                        // without default values
102                        None => Arg::new(variable.name.clone())
103                            .help(help)
104                            .long(long)
105                            .action(ArgAction::SetTrue),
106                        Some(true) => Arg::new(variable.name.clone())
107                            .help(help)
108                            .long(format!("disable-{long}"))
109                            .default_value("true")
110                            .action(ArgAction::SetFalse),
111                        Some(false) => Arg::new(variable.name.clone())
112                            .help(help)
113                            .long(format!("enable-{long}"))
114                            .action(ArgAction::SetTrue),
115                    };
116                    cmd = cmd.arg(arg);
117                } else {
118                    let short = variable.cmdarg.short.to_owned();
119
120                    let default = if let Some(default) = variable
121                        .cmdarg
122                        .default
123                        .to_owned()
124                        .or(variable.value.to_owned())
125                    {
126                        Some(match default {
127                            Value::Number(v) => v.to_string(),
128                            Value::String(v) => v,
129                            _ => bail!(
130                            "We only want to see integers or strings in default values for non-bool variables.."
131                        ),
132                        })
133                    } else {
134                        None
135                    };
136                    let arg = Arg::new(variable.name.clone())
137                        .action(ArgAction::Set)
138                        .help(help)
139                        .long(long);
140                    let arg = if let Some(s) = short {
141                        let chars = s.chars().collect::<Vec<char>>();
142                        if chars.len() != 1 {
143                            bail!(
144                            "Short name for variable `{}` is expected to be just in 1 character",
145                            variable.name
146                        );
147                        }
148
149                        arg.short(chars[0])
150                    } else {
151                        arg
152                    };
153                    // For values with defaults, we set the default ones
154                    // For other values, if they were not provided when parsing, we'll fill the corresponding memory with zero, or report error, based on what we need
155                    let arg = if let Some(default) = default {
156                        arg.default_value(default)
157                    } else {
158                        arg
159                    };
160                    cmd = cmd.arg(arg);
161                }
162            }
163        }
164        Ok(cmd)
165    }
166}
167#[cfg(test)]
168mod tests {
169    use crate::{meta::EunomiaObjectMeta, tests::get_assets_dir};
170
171    #[test]
172    fn test_arg_builder() {
173        let skel = serde_json::from_str::<EunomiaObjectMeta>(
174            &std::fs::read_to_string(get_assets_dir().join("arg_builder_test").join("skel.json"))
175                .unwrap(),
176        )
177        .unwrap();
178        let cmd = skel.build_argument_parser().unwrap();
179        for p in cmd.get_arguments() {
180            println!("{:?}", p.get_long());
181        }
182        let cmd = cmd.color(clap::ColorChoice::Never);
183        let matches = cmd
184            .try_get_matches_from([
185                "myprog",
186                "--cv1",
187                "2333",
188                "--const_val_2",
189                "12345678",
190                "--const_val_3",
191                "abcdefg",
192                "--bss_val_1",
193                "111",
194            ])
195            .unwrap();
196        assert_eq!(
197            matches.get_one::<String>("const_val_1"),
198            Some(&String::from("2333"))
199        );
200        assert_eq!(
201            matches.get_one::<String>("const_val_2"),
202            Some(&String::from("12345678"))
203        );
204        assert_eq!(
205            matches.get_one::<String>("const_val_3"),
206            Some(&String::from("abcdefg"))
207        );
208        assert_eq!(
209            matches.get_one::<String>("bss_val_1"),
210            Some(&String::from("111"))
211        );
212        assert_eq!(matches.get_one::<String>("bss_val_2"), None);
213        assert_eq!(matches.get_one::<String>("bss_val_3"), None);
214    }
215    #[test]
216    #[should_panic]
217    fn test_arg_builder_invalid_argument() {
218        let skel = serde_json::from_str::<EunomiaObjectMeta>(
219            &std::fs::read_to_string(get_assets_dir().join("arg_builder_test").join("skel.json"))
220                .unwrap(),
221        )
222        .unwrap();
223        let cmd = skel.build_argument_parser().unwrap();
224        cmd.try_get_matches_from(["prog", "-a", "123"]).unwrap();
225    }
226    #[test]
227    fn test_boolflag() {
228        let skel = serde_json::from_str::<EunomiaObjectMeta>(
229            &std::fs::read_to_string(get_assets_dir().join("arg_builder_test").join("skel.json"))
230                .unwrap(),
231        )
232        .unwrap();
233        let cmd = skel.build_argument_parser().unwrap();
234        let matches = cmd
235            .clone()
236            .try_get_matches_from([
237                "prog",
238                "--boolflag",
239                "--disable-boolflag-with-default-true",
240                "--enable-boolflag-with-default-false",
241            ])
242            .unwrap();
243        assert!(matches.get_flag("boolflag"));
244        assert!(!matches.get_flag("boolflag-with-default-true"));
245        assert!(matches.get_flag("boolflag-with-default-false"));
246
247        let matches = cmd.clone().try_get_matches_from(["prog"]).unwrap();
248        assert!(!matches.get_flag("boolflag"));
249        assert!(matches.get_flag("boolflag-with-default-true"));
250        assert!(!matches.get_flag("boolflag-with-default-false"));
251    }
252    #[test]
253    #[should_panic]
254    fn test_boolflag_2() {
255        let skel = serde_json::from_str::<EunomiaObjectMeta>(
256            &std::fs::read_to_string(get_assets_dir().join("arg_builder_test").join("skel.json"))
257                .unwrap(),
258        )
259        .unwrap();
260        let cmd = skel.build_argument_parser().unwrap();
261        cmd.clone()
262            .try_get_matches_from(["prog", "--enable-boolflag-with-default-true"])
263            .unwrap();
264    }
265    #[test]
266    #[should_panic]
267    fn test_boolflag_3() {
268        let skel = serde_json::from_str::<EunomiaObjectMeta>(
269            &std::fs::read_to_string(get_assets_dir().join("arg_builder_test").join("skel.json"))
270                .unwrap(),
271        )
272        .unwrap();
273        let cmd = skel.build_argument_parser().unwrap();
274        cmd.clone()
275            .try_get_matches_from(["prog", "--disable-boolflag-with-default-false"])
276            .unwrap();
277    }
278}