manta-cli 1.62.7

Another CLI for ALPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
pub mod types;

use std::{
  collections::HashMap,
  fs::{self, File},
  io::{Read, Write},
  path::PathBuf,
};

use anyhow::Context;
use config::Config;
use dialoguer::{Input, Select};
use directories::ProjectDirs;
use manta_backend_dispatcher::types::{K8sAuth, K8sDetails};
use toml_edit::DocumentMut;
use types::{MantaConfiguration, Site};

use crate::common::{
  audit::Auditor,
  check_network_connectivity::check_network_connectivity_to_backend,
  kafka::Kafka,
};

/// Returns the XDG-compliant `ProjectDirs` for manta.
///
/// All path helpers in this module delegate to this function
/// so the qualifier/organization/application triple is defined
/// in exactly one place.
fn get_project_dirs() -> Result<ProjectDirs, anyhow::Error> {
  ProjectDirs::from(
    "local", /*qualifier*/
    "cscs",  /*organization*/
    "manta", /*application*/
  )
  .context(
    "Could not determine project directories \
     (home directory may not be set)",
  )
}

/// Returns the default manta config directory path
/// (e.g. `~/.config/manta/`).
pub(crate) fn get_default_config_path() -> Result<PathBuf, anyhow::Error> {
  Ok(PathBuf::from(get_project_dirs()?.config_dir()))
}

/// Returns the default manta config file path
/// (e.g. `~/.config/manta/config.toml`).
pub(crate) fn get_default_manta_config_file_path()
-> Result<PathBuf, anyhow::Error> {
  let mut path = get_default_config_path()?;
  path.push("config.toml");
  Ok(path)
}

/// Returns the default manta cache directory path
/// (e.g. `~/.cache/manta/`).
pub(crate) fn get_default_cache_path() -> Result<PathBuf, anyhow::Error> {
  Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
}

/// Reads the manta configuration file and parses it as TOML.
///
/// Returns both the file path (for later writing) and the
/// parsed `DocumentMut`.
pub(crate) fn read_config_toml() -> Result<(PathBuf, DocumentMut), anyhow::Error>
{
  let path = get_default_manta_config_file_path()?;

  log::debug!(
    "Reading manta configuration from {}",
    path.to_string_lossy()
  );

  let content =
    fs::read_to_string(&path).context("Error reading configuration file")?;

  let doc = content
    .parse::<DocumentMut>()
    .context("Could not parse configuration file as TOML")?;

  Ok((path, doc))
}

/// Writes a `DocumentMut` back to the manta configuration file.
pub(crate) fn write_config_toml(
  path: &std::path::Path,
  doc: &DocumentMut,
) -> Result<(), anyhow::Error> {
  let mut file = std::fs::OpenOptions::new()
    .write(true)
    .truncate(true)
    .open(path)
    .context("Failed to open configuration file for writing")?;

  file
    .write_all(doc.to_string().as_bytes())
    .context("Failed to write configuration file")?;
  file.flush().context("Failed to flush configuration file")?;

  Ok(())
}

/// Read the root CA certificate from `file_path`, falling
/// back to the default config directory if the path is
/// relative.
pub fn get_csm_root_cert_content(
  file_path: &str,
) -> Result<Vec<u8>, anyhow::Error> {
  let mut buf = Vec::new();
  let root_cert_file_rslt = File::open(file_path);

  let file_rslt = if root_cert_file_rslt.is_err() {
    let mut config_path = get_default_config_path()?;
    config_path.push(file_path);
    File::open(config_path)
  } else {
    root_cert_file_rslt
  };

  match file_rslt {
    Ok(mut file) => {
      file
        .read_to_end(&mut buf)
        .context("Failed to read CA root certificate file")?;

      Ok(buf)
    }
    Err(_) => Err(anyhow::anyhow!("CA public root file could not be found")),
  }
}

fn get_default_manta_audit_file_path() -> Result<PathBuf, anyhow::Error> {
  let mut log_file_path = PathBuf::from(get_project_dirs()?.data_dir());
  log_file_path.push("manta.log");
  Ok(log_file_path)
}

fn get_default_mgmt_plane_ca_cert_file_path() -> Result<PathBuf, anyhow::Error>
{
  let mut ca_cert_file_path = get_default_config_path()?;
  ca_cert_file_path.push("alps_root_cert.pem");
  Ok(ca_cert_file_path)
}

/// Get Manta configuration full path. Configuration may be the default one or specified by user.
/// This function also validates if the config file is TOML format
pub async fn get_config_file_path() -> Result<PathBuf, anyhow::Error> {
  // Get config file path from ENV var
  if let Ok(env_config_file_name) = std::env::var("MANTA_CONFIG") {
    let mut env_config_file = std::path::PathBuf::new();
    env_config_file.push(env_config_file_name);
    Ok(env_config_file)
  } else {
    // Get default config file path ($XDG_CONFIG/manta/config.toml
    get_default_manta_config_file_path()
  }
}

/// Reads configuration parameters related to manta from environment variables or file. If both
/// defiend, then environment variables takes preference
pub async fn get_configuration() -> Result<Config, anyhow::Error> {
  // Get config file path
  let config_file_path = get_config_file_path().await?;

  // If config file does not exists, then use config file generator to create a default config
  // file
  if !config_file_path.exists() {
    // Configuration file does not exists --> create a new configuration file
    log::info!(
      "Configuration file '{}' not found. Creating a new one.",
      config_file_path.to_string_lossy()
    );
    create_new_config_file(Some(&config_file_path)).await?;
  };

  // Process config file and check format (toml) is correct
  let config_file_path_str = config_file_path.to_str().ok_or_else(|| {
    anyhow::anyhow!("Configuration file path contains invalid UTF-8")
  })?;

  let config_file =
    config::File::new(config_file_path_str, config::FileFormat::Toml);

  // Process config file
  ::config::Config::builder()
    .add_source(config_file)
    .add_source(
      ::config::Environment::with_prefix("MANTA")
        .try_parsing(true)
        .prefix_separator("_"),
    )
    .build()
    .context("Could not process manta configuration file")
}

/// Prompt the user for a string value with a default.
fn prompt_string(
  prompt: &str,
  default: &str,
) -> Result<String, anyhow::Error> {
  Input::new()
    .with_prompt(prompt)
    .default(default.to_string())
    .show_default(true)
    .interact_text()
    .with_context(|| format!("Failed to read: {}", prompt))
}

/// Prompt the user for a string value with a default,
/// allowing empty input.
fn prompt_string_allow_empty(
  prompt: &str,
  default: &str,
) -> Result<String, anyhow::Error> {
  Input::new()
    .with_prompt(prompt)
    .default(default.to_string())
    .show_default(true)
    .allow_empty(true)
    .interact_text()
    .with_context(|| format!("Failed to read: {}", prompt))
}

async fn create_new_config_file(
  config_file_path_opt: Option<&PathBuf>,
) -> Result<(), anyhow::Error> {
  eprintln!("Configuration file not found. Please introduce values below:");

  let log_level_values = vec!["error", "info", "warn", "debug", "trace"];

  let log_selection = Select::new()
    .with_prompt("Please select 'log verbosity' level from the list below")
    .items(&log_level_values)
    .default(0)
    .interact()
    .context("Failed to read log level selection")?;

  let log = log_level_values[log_selection].to_string();

  let parent_hsm_group = String::new();

  let audit_file: String = prompt_string_allow_empty(
    "Please type full path for the audit file",
    &get_default_manta_audit_file_path()?
      .to_string_lossy()
      .to_string(),
  )?;

  let site: String = prompt_string(
    "Please type site name",
    "alps",
  )?;

  let shasta_base_url: String = prompt_string(
    "Please type site management plane URL",
    "https://api.cmn.alps.cscs.ch",
  )?;

  let k8s_api_url: String = prompt_string(
    "Please type kubernetes api URL",
    "https://10.252.1.12:6442",
  )?;

  let vault_base_url: String = prompt_string(
    "Please type Hashicorp Vault URL",
    "https://hashicorp-vault.cscs.ch:8200",
  )?;

  let vault_secret_path: String = prompt_string(
    "Please type Hashicorp Vault secret path",
    "shasta",
  )?;

  let root_ca_cert_file: String = prompt_string(
    "Please type full path for the CA public certificate file",
    &get_default_mgmt_plane_ca_cert_file_path()?
      .to_string_lossy()
      .to_string(),
  )?;

  let backend_options = vec!["csm", "ochami"];

  let backend_selection = Select::new()
    .with_prompt("Please select 'backend' technology from the list below")
    .items(&backend_options)
    .default(0)
    .interact()
    .context("Failed to read backend selection")?;

  let backend = backend_options[backend_selection].to_string();

  // Broker is optional value
  let audit_kafka_brokers: String = prompt_string_allow_empty(
    "Please type kafka broker to send audit logs",
    "kafka.o11y.cscs.ch:9095",
  )
  .unwrap_or_default();

  let audit_kafka_topic: String = if !audit_kafka_brokers.is_empty() {
    prompt_string(
      "Please type kafka topic to send audit logs",
      "test-topic",
    )
    .unwrap_or_default()
  } else {
    String::new()
  };

  // Create auditor only when both broker and topic are provided
  let auditor =
    if !audit_kafka_brokers.is_empty() && !audit_kafka_topic.is_empty() {
      let kafka = Kafka::new(vec![audit_kafka_brokers], audit_kafka_topic);

      Some(Auditor { kafka })
    } else {
      None
    };

  println!("Testing connectivity to CSM backend, please wait ...");

  let test_backend_api =
    check_network_connectivity_to_backend(&shasta_base_url).await;

  let socks5_proxy = if test_backend_api.is_ok() {
    println!("This machine can access CSM API, no need to setup SOCKS5 proxy");
    None
  } else {
    println!("This machine cannot access CSM API, configuring SOCKS5 proxy");

    // Get the right socks5 proxy value based on if client can reach backend api or not
    Some(prompt_string_allow_empty(
      "Please type socks5 proxy URL",
      "socks5h://127.0.0.1:1080",
    )?)
  };

  let k8s_auth = K8sAuth::Native {
    certificate_authority_data: String::new(),
    client_certificate_data: String::new(),
    client_key_data: String::new(),
  };

  let k8s_details = K8sDetails {
    api_url: k8s_api_url.clone(),
    authentication: k8s_auth,
  };

  let site_details = Site {
    socks5_proxy,
    shasta_base_url,
    vault_base_url: Some(vault_base_url),
    vault_secret_path: Some(vault_secret_path),
    root_ca_cert_file,
    k8s: Some(k8s_details),
    backend,
  };

  let mut site_hashmap = HashMap::new();
  site_hashmap.insert(site.clone(), site_details);

  let config_toml = MantaConfiguration {
    log,
    site,
    parent_hsm_group,
    audit_file,
    sites: site_hashmap,
    auditor,
  };

  let config_file_content = toml::to_string(&config_toml)
    .context("Failed to serialize configuration to TOML")?;

  // Create configuration file on user's location, otherwise use default path
  //
  // Create PathBuf to store the manta config file specified by user or get default one
  let config_file_path = if let Some(config_file_path) = config_file_path_opt {
    PathBuf::from(config_file_path)
  } else {
    get_default_manta_config_file_path()?
  };

  // Create directories if needed
  let parent_dir = config_file_path
    .parent()
    .context("Configuration file path has no parent directory")?;
  std::fs::create_dir_all(parent_dir).with_context(|| {
    format!(
      "Failed to create config directory '{}'",
      parent_dir.display()
    )
  })?;

  // Create manta config file
  let mut config_file = File::create(&config_file_path).with_context(|| {
    format!(
      "Failed to create config file '{}'",
      config_file_path.display()
    )
  })?;
  config_file
    .write_all(config_file_content.as_bytes())
    .with_context(|| {
      format!(
        "Failed to write config file '{}'",
        config_file_path.display()
      )
    })?;

  log::info!(
    "Configuration file '{}' created",
    config_file_path.to_string_lossy()
  );

  Ok(())
}