chiark / gitweb /
wip impl config reading
[hippotat.git] / src / config.rs
1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 use configparser::ini::Ini;
8
9 static MAIN_CONFIGS: [&str;_] = ["main.cfg", "config.d", "secrets.d"];
10
11 #[derive(StructOpt,Debug)]
12 pub struct Opts {
13   /// Top-level config file or directory
14   ///
15   /// Look for `main.cfg`, `config.d` and `secrets.d` here.
16   ///
17   /// Or if this is a file, just read that file.
18   #[structopt(long, default_value="/etc/hippotat")]
19   pub config: PathBuf,
20   
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>,
24 }
25
26 pub struct CidrString(pub String);
27
28 pub struct InstanceConfig {
29   // Exceptional settings
30   pub server:                       String,
31   pub secret:                       String,
32   pub ipif:                         String,
33
34   // Capped settings:
35   pub max_batch_down:               u32,
36   pub max_queue_time:               Duration,
37   pub http_timeout:                 Duration,
38
39   // Ordinary settings:
40   pub target_requests_outstanding:  u32,
41   pub addrs:                        Vec<IpAddr>,
42   pub vnetwork:                     Vec<CidrString>,
43   pub vaddr:                        Vec<IpAddr>,
44   pub vrelay:                       IpAddr,
45   pub port:                         u16,
46   pub mtu:                          u32,
47   pub ifname_server:                String,
48   pub ifname_client:                String,
49
50   // Ordinary settings, used by server only:
51   pub max_clock_skew:               Duration,
52
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,
58   pub url:                          Uri,
59   pub vroutes:                      Vec<CidrString>,
60 }
61
62 type SectionMap = HashMap<String, Option<String>>;
63
64 pub struct Config {
65   opts: Opts,
66 }
67
68 static OUTSIDE_SECTION: &str = "[";
69
70 struct Aggregate {
71   sections: HashMap<String, SectionMap>,
72 }
73
74 impl Aggregate {
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>
79   {
80     let f = match File::open(path) {
81       Err(e) if ekok(e.kind()) => return Err(e.kind()),
82       r => r.context("open")?;
83     }
84
85     let mut s = String::new();
86     f.read_to_string(&mut s).context("read")?;
87
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"));
94     }
95
96     // xxx parse section names here
97     // xxx save Arc<PathBuf> where we found each item
98
99     self.sections.extend(map.drain());
100     Ok(())
101   }
102
103   #[throws(AE)] // AE includes path
104   fn read_dir_d(&mut self, path: Path
105                 ekok: &Fn(ErrorKind))
106                 -> Result<(), io::ErrorKind>
107   {
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)?;
112     } {
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()
119       ) { continue }
120
121       // OK we want this one
122       self.read_file(&ent.path, &|_| false)
123         .with_context(format!("{:?}", &ent.path))??;
124     }
125   }
126
127   #[throws(AE)] // AE includes everything
128   fn read_toplevel(&mut self, toplevel: &Path) {
129     match se;lf.read_file(
130       toplevel,
131       |k| matches!(k, EK::NotFound || EK::IsDirectory)
132     )
133       .with_context(format!("{:?}", toplevel))
134       .context("top-level config directory (or file)")?
135     {
136       Err(EK::NotFound) => { },
137
138       Err(EK::IsDirectory) => {
139         let mk = |leaf| [ toplevel, leaf ].iter().collect::<PathBuf>();
140
141         for &(try_main, desc) in &[
142           ("main.cfg", "main config file"),
143           ("master.cfg", "obsolete-named main config file"),
144         ] {
145           let main = mk(try_main);
146           match self.read_file(&main, |e| e == EK::NotFound)
147             .with_context(format!("{:?}", &main))
148             .context(desc)?
149           {
150             Ok(()) => break,
151             Err(EK::NotFound) => { },
152             x => panic!("huh? {:?}", &x),
153           }
154         }
155
156         for &(try_dir, desc) in &[
157           ("config.d", "per-link config directory"),
158           ("secrets.d", "per-link secrets directory"),
159         ] {
160           let dir = mk(try_dir);
161           match agg.read_dir(&dir, |k| k == EK::NotFound).context(desc)? {
162             Ok(()) => { },
163             Err(EK::NotFound) => { },
164             x => panic!("huh? {:?}", &x),
165           }
166         }
167       }
168     }
169   }
170
171   #[throws(AE)] // AE includes extra, but does that this is extra
172   fn read_extra(&mut self, extra: &Path) {
173
174     match self.read_file(extra, |k| k == EK::IsDirectory)
175       .with_context(format!("{:?}", extra))?
176     {
177       Ok(()) => return,
178       Err(EK::IsDirectory) => { }
179       x => panic!("huh? {:?}", &x),
180     }
181
182     self.read_dir(extra, |_| false)??;
183   }
184 }
185
186
187 #[throws(AE)]
188 pub fn read() {
189   let opts = config::Opts::from_args();
190
191   (||{
192     let agg = Aggregate::default();
193
194     agg.read_toplevel(&opts.config)?;
195     for extra in &opts.extra_config {
196       agg.read_extra(extra).context("extra config")?;
197     }
198
199     eprintln!("GOT {:?}", agg);
200
201     Ok::<_,AE>();
202   }).context("read configuration");
203 }