1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
7 use configparser::ini::Ini;
9 static MAIN_CONFIGS: [&str;_] = ["main.cfg", "config.d", "secrets.d"];
11 #[derive(StructOpt,Debug)]
13 /// Top-level config file or directory
15 /// Look for `main.cfg`, `config.d` and `secrets.d` here.
17 /// Or if this is a file, just read that file.
18 #[structopt(long, default_value="/etc/hippotat")]
21 /// Additional config files or dirs, which can override the others
22 #[structopt(long, multiple=true, number_of_values=1)]
23 pub extra_config: Vec<PathBuf>,
26 pub struct CidrString(pub String);
28 pub struct InstanceConfig {
29 // Exceptional settings
35 pub max_batch_down: u32,
36 pub max_queue_time: Duration,
37 pub http_timeout: Duration,
40 pub target_requests_outstanding: u32,
41 pub addrs: Vec<IpAddr>,
42 pub vnetwork: Vec<CidrString>,
43 pub vaddr: Vec<IpAddr>,
47 pub ifname_server: String,
48 pub ifname_client: String,
50 // Ordinary settings, used by server only:
51 pub max_clock_skew: Duration,
53 // Ordinary settings, used by client only:
54 pub http_timeout_grace: Duration,
55 pub max_requests_outstanding: u32,
56 pub max_batch_up: u32,
57 pub http_retry: Duration,
59 pub vroutes: Vec<CidrString>,
62 type SectionMap = HashMap<String, Option<String>>;
68 static OUTSIDE_SECTION: &str = "[";
71 sections: HashMap<String, SectionMap>,
75 #[throws(AE)] // AE does not include path
76 fn read_file(&mut self, path: Path,
77 ekok: &Fn(ErrorKind) -> Result<(),()>)
78 -> Result<(), io::ErrorKind>
80 let f = match File::open(path) {
81 Err(e) if ekok(e.kind()) => return Err(e.kind()),
82 r => r.context("open")?;
85 let mut s = String::new();
86 f.read_to_string(&mut s).context("read")?;
88 let mut ini = configparser::ini::Ini::new_cs();
89 ini.set_default_section(OUTSIDE_SECTION);
90 ini.read(s).context("parse as INI");
91 let map = mem::take(ini.get_mut_map);
92 if map.get(OUTSIDE_SECTION).is_some() {
93 throw!(anyhow!("INI file contains settings outside a section"));
96 // xxx parse section names here
97 // xxx save Arc<PathBuf> where we found each item
99 self.sections.extend(map.drain());
103 #[throws(AE)] // AE includes path
104 fn read_dir_d(&mut self, path: Path
105 ekok: &Fn(ErrorKind))
106 -> Result<(), io::ErrorKind>
108 let wc = || format("{:?}", path);
109 for ent in match fs::read_dir(path) {
110 Err(e) if ekok(e.kind()) => return Err(e.kind()),
111 r => r.context("open directory").with_context(wc)?;
113 let ent = ent.context("read directory").with_context(wc)?;
114 let leaf = ent.file_name().as_str();
115 let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
116 if leaf.length() == 0 { continue }
117 if ! leaf.chars().all(
118 |c| c=='-' || c=='_' || c.is_ascii_alphenumeric()
121 // OK we want this one
122 self.read_file(&ent.path, &|_| false)
123 .with_context(format!("{:?}", &ent.path))??;
127 #[throws(AE)] // AE includes everything
128 fn read_toplevel(&mut self, toplevel: &Path) {
129 match se;lf.read_file(
131 |k| matches!(k, EK::NotFound || EK::IsDirectory)
133 .with_context(format!("{:?}", toplevel))
134 .context("top-level config directory (or file)")?
136 Err(EK::NotFound) => { },
138 Err(EK::IsDirectory) => {
139 let mk = |leaf| [ toplevel, leaf ].iter().collect::<PathBuf>();
141 for &(try_main, desc) in &[
142 ("main.cfg", "main config file"),
143 ("master.cfg", "obsolete-named main config file"),
145 let main = mk(try_main);
146 match self.read_file(&main, |e| e == EK::NotFound)
147 .with_context(format!("{:?}", &main))
151 Err(EK::NotFound) => { },
152 x => panic!("huh? {:?}", &x),
156 for &(try_dir, desc) in &[
157 ("config.d", "per-link config directory"),
158 ("secrets.d", "per-link secrets directory"),
160 let dir = mk(try_dir);
161 match agg.read_dir(&dir, |k| k == EK::NotFound).context(desc)? {
163 Err(EK::NotFound) => { },
164 x => panic!("huh? {:?}", &x),
171 #[throws(AE)] // AE includes extra, but does that this is extra
172 fn read_extra(&mut self, extra: &Path) {
174 match self.read_file(extra, |k| k == EK::IsDirectory)
175 .with_context(format!("{:?}", extra))?
178 Err(EK::IsDirectory) => { }
179 x => panic!("huh? {:?}", &x),
182 self.read_dir(extra, |_| false)??;
189 let opts = config::Opts::from_args();
192 let agg = Aggregate::default();
194 agg.read_toplevel(&opts.config)?;
195 for extra in &opts.extra_config {
196 agg.read_extra(extra).context("extra config")?;
199 eprintln!("GOT {:?}", agg);
202 }).context("read configuration");