1use ant_bootstrap::InitialPeersConfig;
10use ant_evm::{EvmNetwork, RewardsAddress};
11use ant_logging::LogFormat;
12use ant_service_management::node::push_arguments_from_initial_peers_config;
13use color_eyre::{Result, eyre::eyre};
14use service_manager::{ServiceInstallCtx, ServiceLabel};
15use std::{
16 ffi::OsString,
17 net::{Ipv4Addr, SocketAddr},
18 path::PathBuf,
19 str::FromStr,
20};
21
22#[derive(Clone, Debug)]
23pub enum PortRange {
24 Single(u16),
25 Range(u16, u16),
26}
27
28impl PortRange {
29 pub fn parse(s: &str) -> Result<Self> {
30 if let Ok(port) = u16::from_str(s) {
31 Ok(Self::Single(port))
32 } else {
33 let parts: Vec<&str> = s.split('-').collect();
34 if parts.len() != 2 {
35 return Err(eyre!("Port range must be in the format 'start-end'"));
36 }
37 let start = parts[0].parse::<u16>()?;
38 let end = parts[1].parse::<u16>()?;
39 if start >= end {
40 return Err(eyre!("End port must be greater than start port"));
41 }
42 Ok(Self::Range(start, end))
43 }
44 }
45
46 pub fn validate(&self, count: u16) -> Result<()> {
48 match self {
49 Self::Single(_) => {
50 if count != 1 {
51 error!("The count ({count}) does not match the number of ports (1)");
52 return Err(eyre!(
53 "The count ({count}) does not match the number of ports (1)"
54 ));
55 }
56 }
57 Self::Range(start, end) => {
58 let port_count = end - start + 1;
59 if count != port_count {
60 error!("The count ({count}) does not match the number of ports ({port_count})");
61 return Err(eyre!(
62 "The count ({count}) does not match the number of ports ({port_count})"
63 ));
64 }
65 }
66 }
67 Ok(())
68 }
69}
70
71#[derive(Debug, PartialEq)]
72pub struct InstallNodeServiceCtxBuilder {
73 pub alpha: bool,
74 pub antnode_path: PathBuf,
75 pub autostart: bool,
76 pub data_dir_path: PathBuf,
77 pub env_variables: Option<Vec<(String, String)>>,
78 pub evm_network: EvmNetwork,
79 pub log_dir_path: PathBuf,
80 pub log_format: Option<LogFormat>,
81 pub name: String,
82 pub network_id: Option<u8>,
83 pub no_upnp: bool,
84 pub max_archived_log_files: Option<usize>,
85 pub max_log_files: Option<usize>,
86 pub metrics_port: Option<u16>,
87 pub node_ip: Option<Ipv4Addr>,
88 pub node_port: Option<u16>,
89 pub init_peers_config: InitialPeersConfig,
90 pub rewards_address: RewardsAddress,
91 pub relay: bool,
92 pub rpc_socket_addr: SocketAddr,
93 pub service_user: Option<String>,
94 pub write_older_cache_files: bool,
95}
96
97impl InstallNodeServiceCtxBuilder {
98 pub fn build(self) -> Result<ServiceInstallCtx> {
99 let label: ServiceLabel = self.name.parse()?;
100 let mut args = vec![
101 OsString::from("--rpc"),
102 OsString::from(self.rpc_socket_addr.to_string()),
103 OsString::from("--root-dir"),
104 OsString::from(self.data_dir_path.to_string_lossy().to_string()),
105 OsString::from("--log-output-dest"),
106 OsString::from(self.log_dir_path.to_string_lossy().to_string()),
107 ];
108
109 push_arguments_from_initial_peers_config(&self.init_peers_config, &mut args);
110 if self.alpha {
111 args.push(OsString::from("--alpha"));
112 }
113 if let Some(id) = self.network_id {
114 args.push(OsString::from("--network-id"));
115 args.push(OsString::from(id.to_string()));
116 }
117 if self.relay {
118 args.push(OsString::from("--relay"));
119 }
120 if let Some(log_format) = self.log_format {
121 args.push(OsString::from("--log-format"));
122 args.push(OsString::from(log_format.as_str()));
123 }
124 if self.no_upnp {
125 args.push(OsString::from("--no-upnp"));
126 }
127 if let Some(node_ip) = self.node_ip {
128 args.push(OsString::from("--ip"));
129 args.push(OsString::from(node_ip.to_string()));
130 }
131 if let Some(node_port) = self.node_port {
132 args.push(OsString::from("--port"));
133 args.push(OsString::from(node_port.to_string()));
134 }
135 if let Some(metrics_port) = self.metrics_port {
136 args.push(OsString::from("--metrics-server-port"));
137 args.push(OsString::from(metrics_port.to_string()));
138 }
139 if let Some(log_files) = self.max_archived_log_files {
140 args.push(OsString::from("--max-archived-log-files"));
141 args.push(OsString::from(log_files.to_string()));
142 }
143 if let Some(log_files) = self.max_log_files {
144 args.push(OsString::from("--max-log-files"));
145 args.push(OsString::from(log_files.to_string()));
146 }
147
148 args.push(OsString::from("--rewards-address"));
149 args.push(OsString::from(self.rewards_address.to_string()));
150 if self.write_older_cache_files {
151 args.push(OsString::from("--write-older-cache-files"));
152 }
153
154 args.push(OsString::from(self.evm_network.to_string()));
155 if let EvmNetwork::Custom(custom_network) = &self.evm_network {
156 args.push(OsString::from("--rpc-url"));
157 args.push(OsString::from(custom_network.rpc_url_http.to_string()));
158 args.push(OsString::from("--payment-token-address"));
159 args.push(OsString::from(
160 custom_network.payment_token_address.to_string(),
161 ));
162 args.push(OsString::from("--data-payments-address"));
163 args.push(OsString::from(
164 custom_network.data_payments_address.to_string(),
165 ));
166 }
167
168 Ok(ServiceInstallCtx {
169 args,
170 autostart: self.autostart,
171 contents: None,
172 environment: self.env_variables,
173 label: label.clone(),
174 program: self.antnode_path.to_path_buf(),
175 username: self.service_user.clone(),
176 working_directory: None,
177 disable_restart_on_failure: true,
178 })
179 }
180}
181
182pub struct AddNodeServiceOptions {
183 pub alpha: bool,
184 pub antnode_dir_path: PathBuf,
185 pub antnode_src_path: PathBuf,
186 pub auto_restart: bool,
187 pub auto_set_nat_flags: bool,
188 pub count: Option<u16>,
189 pub delete_antnode_src: bool,
190 pub enable_metrics_server: bool,
191 pub env_variables: Option<Vec<(String, String)>>,
192 pub evm_network: EvmNetwork,
193 pub init_peers_config: InitialPeersConfig,
194 pub log_format: Option<LogFormat>,
195 pub max_archived_log_files: Option<usize>,
196 pub max_log_files: Option<usize>,
197 pub metrics_port: Option<PortRange>,
198 pub network_id: Option<u8>,
199 pub node_ip: Option<Ipv4Addr>,
200 pub node_port: Option<PortRange>,
201 pub no_upnp: bool,
202 pub relay: bool,
203 pub rewards_address: RewardsAddress,
204 pub rpc_address: Option<Ipv4Addr>,
205 pub rpc_port: Option<PortRange>,
206 pub service_data_dir_path: PathBuf,
207 pub service_log_dir_path: PathBuf,
208 pub user: Option<String>,
209 pub user_mode: bool,
210 pub version: String,
211 pub write_older_cache_files: bool,
212}
213
214pub struct AddDaemonServiceOptions {
215 pub address: Ipv4Addr,
216 pub env_variables: Option<Vec<(String, String)>>,
217 pub daemon_install_bin_path: PathBuf,
218 pub daemon_src_bin_path: PathBuf,
219 pub port: u16,
220 pub user: String,
221 pub version: String,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use ant_evm::{CustomNetwork, RewardsAddress};
228 use std::net::{IpAddr, Ipv4Addr};
229
230 fn create_default_builder() -> InstallNodeServiceCtxBuilder {
231 InstallNodeServiceCtxBuilder {
232 alpha: false,
233 antnode_path: PathBuf::from("/bin/antnode"),
234 autostart: true,
235 data_dir_path: PathBuf::from("/data"),
236 env_variables: None,
237 evm_network: EvmNetwork::ArbitrumOne,
238 relay: false,
239 log_dir_path: PathBuf::from("/logs"),
240 log_format: None,
241 max_archived_log_files: None,
242 max_log_files: None,
243 metrics_port: None,
244 name: "test-node".to_string(),
245 network_id: None,
246 no_upnp: false,
247 node_ip: None,
248 node_port: None,
249 init_peers_config: InitialPeersConfig::default(),
250 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
251 .unwrap(),
252 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
253 service_user: None,
254 write_older_cache_files: false,
255 }
256 }
257
258 fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
259 InstallNodeServiceCtxBuilder {
260 alpha: false,
261 autostart: true,
262 data_dir_path: PathBuf::from("/data"),
263 env_variables: None,
264 evm_network: EvmNetwork::Custom(CustomNetwork {
265 rpc_url_http: "http://localhost:8545".parse().unwrap(),
266 payment_token_address: RewardsAddress::from_str(
267 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
268 )
269 .unwrap(),
270 data_payments_address: RewardsAddress::from_str(
271 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
272 )
273 .unwrap(),
274 }),
275 relay: false,
276 log_dir_path: PathBuf::from("/logs"),
277 log_format: None,
278 max_archived_log_files: None,
279 max_log_files: None,
280 metrics_port: None,
281 name: "test-node".to_string(),
282 network_id: None,
283 node_ip: None,
284 node_port: None,
285 init_peers_config: InitialPeersConfig::default(),
286 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
287 .unwrap(),
288 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
289 antnode_path: PathBuf::from("/bin/antnode"),
290 service_user: None,
291 no_upnp: false,
292 write_older_cache_files: false,
293 }
294 }
295
296 fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
297 InstallNodeServiceCtxBuilder {
298 alpha: true,
299 autostart: true,
300 data_dir_path: PathBuf::from("/data"),
301 env_variables: None,
302 evm_network: EvmNetwork::Custom(CustomNetwork {
303 rpc_url_http: "http://localhost:8545".parse().unwrap(),
304 payment_token_address: RewardsAddress::from_str(
305 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
306 )
307 .unwrap(),
308 data_payments_address: RewardsAddress::from_str(
309 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
310 )
311 .unwrap(),
312 }),
313 relay: false,
314 log_dir_path: PathBuf::from("/logs"),
315 log_format: None,
316 max_archived_log_files: Some(10),
317 max_log_files: Some(10),
318 metrics_port: None,
319 name: "test-node".to_string(),
320 network_id: Some(5),
321 node_ip: None,
322 node_port: None,
323 init_peers_config: InitialPeersConfig::default(),
324 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
325 .unwrap(),
326 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
327 antnode_path: PathBuf::from("/bin/antnode"),
328 service_user: None,
329 no_upnp: false,
330 write_older_cache_files: false,
331 }
332 }
333
334 #[test]
335 fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
336 let builder = create_default_builder();
337 let result = builder.build().unwrap();
338
339 assert_eq!(result.label.to_string(), "test-node");
340 assert_eq!(result.program, PathBuf::from("/bin/antnode"));
341 assert!(result.autostart);
342 assert_eq!(result.username, None);
343 assert_eq!(result.working_directory, None);
344
345 let expected_args = vec![
346 "--rpc",
347 "127.0.0.1:8080",
348 "--root-dir",
349 "/data",
350 "--log-output-dest",
351 "/logs",
352 "--rewards-address",
353 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
354 "evm-arbitrum-one",
355 ];
356 assert_eq!(
357 result
358 .args
359 .iter()
360 .map(|os| os.to_str().unwrap())
361 .collect::<Vec<_>>(),
362 expected_args
363 );
364 }
365
366 #[test]
367 fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
368 let builder = create_custom_evm_network_builder();
369 let result = builder.build().unwrap();
370
371 assert_eq!(result.label.to_string(), "test-node");
372 assert_eq!(result.program, PathBuf::from("/bin/antnode"));
373 assert!(result.autostart);
374 assert_eq!(result.username, None);
375 assert_eq!(result.working_directory, None);
376
377 let expected_args = vec![
378 "--rpc",
379 "127.0.0.1:8080",
380 "--root-dir",
381 "/data",
382 "--log-output-dest",
383 "/logs",
384 "--rewards-address",
385 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
386 "evm-custom",
387 "--rpc-url",
388 "http://localhost:8545/",
389 "--payment-token-address",
390 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
391 "--data-payments-address",
392 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
393 ];
394 assert_eq!(
395 result
396 .args
397 .iter()
398 .map(|os| os.to_str().unwrap())
399 .collect::<Vec<_>>(),
400 expected_args
401 );
402 }
403
404 #[test]
405 fn build_should_assign_expected_values_when_all_options_are_enabled() {
406 let mut builder = create_builder_with_all_options_enabled();
407 builder.alpha = true;
408 builder.relay = true;
409 builder.log_format = Some(LogFormat::Json);
410 builder.no_upnp = true;
411 builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
412 builder.node_port = Some(12345);
413 builder.metrics_port = Some(9090);
414 builder.init_peers_config.addrs = vec![
415 "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
416 "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
417 ];
418 builder.init_peers_config.first = true;
419 builder.init_peers_config.local = true;
420 builder.init_peers_config.network_contacts_url =
421 vec!["http://localhost:8080".parse().unwrap()];
422 builder.init_peers_config.ignore_cache = true;
423 builder.service_user = Some("antnode-user".to_string());
424 builder.write_older_cache_files = true;
425
426 let result = builder.build().unwrap();
427
428 let expected_args = vec![
429 "--rpc",
430 "127.0.0.1:8080",
431 "--root-dir",
432 "/data",
433 "--log-output-dest",
434 "/logs",
435 "--first",
436 "--local",
437 "--peer",
438 "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
439 "--network-contacts-url",
440 "http://localhost:8080",
441 "--ignore-cache",
442 "--alpha",
443 "--network-id",
444 "5",
445 "--relay",
446 "--log-format",
447 "json",
448 "--no-upnp",
449 "--ip",
450 "192.168.1.1",
451 "--port",
452 "12345",
453 "--metrics-server-port",
454 "9090",
455 "--max-archived-log-files",
456 "10",
457 "--max-log-files",
458 "10",
459 "--rewards-address",
460 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
461 "--write-older-cache-files",
462 "evm-custom",
463 "--rpc-url",
464 "http://localhost:8545/",
465 "--payment-token-address",
466 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
467 "--data-payments-address",
468 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
469 ];
470 assert_eq!(
471 result
472 .args
473 .iter()
474 .map(|os| os.to_str().unwrap())
475 .collect::<Vec<_>>(),
476 expected_args
477 );
478 assert_eq!(result.username, Some("antnode-user".to_string()));
479 }
480
481 #[test]
482 fn build_should_assign_expected_values_when_environment_variables_are_provided() {
483 let mut builder = create_default_builder();
484 builder.env_variables = Some(vec![
485 ("VAR1".to_string(), "value1".to_string()),
486 ("VAR2".to_string(), "value2".to_string()),
487 ]);
488
489 let result = builder.build().unwrap();
490
491 assert_eq!(
492 result.environment,
493 Some(vec![
494 ("VAR1".to_string(), "value1".to_string()),
495 ("VAR2".to_string(), "value2".to_string()),
496 ])
497 );
498 }
499}