1818from termcolor import colored
1919
2020from . import dns , remote
21- from .sshexec import LocalExec , SSHExec
21+ from .lxc .cli import ( # noqa: F401
22+ lxc_start_cmd ,
23+ lxc_start_cmd_options ,
24+ lxc_status_cmd ,
25+ lxc_status_cmd_options ,
26+ lxc_stop_cmd ,
27+ lxc_stop_cmd_options ,
28+ lxc_test_cmd ,
29+ lxc_test_cmd_options ,
30+ )
31+ from .sshexec import (
32+ LocalExec ,
33+ SSHExec ,
34+ resolve_host_from_ssh_config ,
35+ resolve_key_from_ssh_config ,
36+ )
37+ from .www import main as webdev_main
2238
2339#
2440# cmdeploy sub commands and options
@@ -82,13 +98,16 @@ def run_cmd_options(parser):
8298 help = "disable checks nslookup for dns" ,
8399 )
84100 add_ssh_host_option (parser )
101+ add_ssh_config_option (parser )
102+
103+
85104
86105
87106def run_cmd (args , out ):
88107 """Deploy chatmail services on the remote server."""
89108
90109 ssh_host = args .ssh_host if args .ssh_host else args .config .mail_domain
91- sshexec = get_sshexec (ssh_host )
110+ sshexec = get_sshexec (ssh_host , ssh_config = args . ssh_config )
92111 require_iroh = args .config .enable_iroh_relay
93112 strict_tls = args .config .tls_cert_mode == "acme"
94113 if not args .dns_check_disabled :
@@ -108,6 +127,18 @@ def run_cmd(args, out):
108127 pyinf = "pyinfra --dry" if args .dry_run else "pyinfra"
109128
110129 cmd = f"{ pyinf } --ssh-user root { ssh_host } { deploy_path } -y"
130+ ssh_config = args .ssh_config
131+ if ssh_config :
132+ ssh_config = str (Path (ssh_config ).resolve ())
133+
134+ # Use pyinfra's native SSH data keys to configure the connection directly
135+ # rather than relying on paramiko config parsing (see also sshexec.py)
136+ ip = resolve_host_from_ssh_config (ssh_host , ssh_config )
137+ key = resolve_key_from_ssh_config (ssh_host , ssh_config )
138+ data_args = f"--data ssh_hostname={ ip } --data ssh_known_hosts_file=/dev/null"
139+ if key :
140+ data_args += f" --data ssh_key={ key } "
141+ cmd = f"{ pyinf } --ssh-user root { ssh_host } { deploy_path } -y { data_args } "
111142 if ssh_host in ["localhost" , "@docker" ]:
112143 if ssh_host == "@docker" :
113144 env ["CHATMAIL_NOPORTCHECK" ] = "True"
@@ -139,15 +170,16 @@ def dns_cmd_options(parser):
139170 dest = "zonefile" ,
140171 type = pathlib .Path ,
141172 default = None ,
142- help = "write out a zonefile " ,
173+ help = "write DNS records in standard BIND format to the given file " ,
143174 )
144175 add_ssh_host_option (parser )
176+ add_ssh_config_option (parser )
145177
146178
147179def dns_cmd (args , out ):
148180 """Check DNS entries and optionally generate dns zone file."""
149181 ssh_host = args .ssh_host if args .ssh_host else args .config .mail_domain
150- sshexec = get_sshexec (ssh_host , verbose = args .verbose )
182+ sshexec = get_sshexec (ssh_host , verbose = args .verbose , ssh_config = args . ssh_config )
151183 tls_cert_mode = args .config .tls_cert_mode
152184 strict_tls = tls_cert_mode == "acme"
153185 remote_data = dns .get_initial_remote_data (sshexec , args .config .mail_domain )
@@ -178,13 +210,14 @@ def dns_cmd(args, out):
178210
179211def status_cmd_options (parser ):
180212 add_ssh_host_option (parser )
213+ add_ssh_config_option (parser )
181214
182215
183216def status_cmd (args , out ):
184217 """Display status for online chatmail instance."""
185218
186219 ssh_host = args .ssh_host if args .ssh_host else args .config .mail_domain
187- sshexec = get_sshexec (ssh_host , verbose = args .verbose )
220+ sshexec = get_sshexec (ssh_host , verbose = args .verbose , ssh_config = args . ssh_config )
188221
189222 out .green (f"chatmail domain: { args .config .mail_domain } " )
190223 if args .config .privacy_mail :
@@ -204,14 +237,18 @@ def test_cmd_options(parser):
204237 help = "also run slow tests" ,
205238 )
206239 add_ssh_host_option (parser )
240+ add_ssh_config_option (parser )
207241
208242
209243def test_cmd (args , out ):
210244 """Run local and online tests for chatmail deployment."""
211245
212246 env = os .environ .copy ()
247+ env ["CHATMAIL_INI" ] = str (args .inipath .resolve ())
213248 if args .ssh_host :
214249 env ["CHATMAIL_SSH" ] = args .ssh_host
250+ if args .ssh_config :
251+ env ["CHATMAIL_SSH_CONFIG" ] = str (Path (args .ssh_config ).resolve ())
215252
216253 pytest_path = shutil .which ("pytest" )
217254 pytest_args = [
@@ -276,9 +313,7 @@ def bench_cmd(args, out):
276313
277314def webdev_cmd (args , out ):
278315 """Run local web development loop for static web pages."""
279- from .www import main
280-
281- main ()
316+ webdev_main ()
282317
283318
284319#
@@ -321,6 +356,16 @@ def add_ssh_host_option(parser):
321356 )
322357
323358
359+ def add_ssh_config_option (parser ):
360+ parser .add_argument (
361+ "--ssh-config" ,
362+ dest = "ssh_config" ,
363+ type = Path ,
364+ default = None ,
365+ help = "Path to an SSH config file (e.g. lxconfigs/ssh-config)." ,
366+ )
367+
368+
324369def add_config_option (parser ):
325370 parser .add_argument (
326371 "--config" ,
@@ -330,6 +375,7 @@ def add_config_option(parser):
330375 type = Path ,
331376 help = "path to the chatmail.ini file" ,
332377 )
378+
333379 parser .add_argument (
334380 "--verbose" ,
335381 "-v" ,
@@ -340,15 +386,16 @@ def add_config_option(parser):
340386 )
341387
342388
343- def add_subcommand (subparsers , func ):
389+ def add_subcommand (subparsers , func , add_config = True ):
344390 name = func .__name__
345391 assert name .endswith ("_cmd" )
346- name = name [:- 4 ]
392+ name = name [:- 4 ]. replace ( "_" , "-" )
347393 doc = func .__doc__ .strip ()
348394 help = doc .split ("\n " )[0 ].strip ("." )
349395 p = subparsers .add_parser (name , description = doc , help = help )
350396 p .set_defaults (func = func )
351- add_config_option (p )
397+ if add_config :
398+ add_config_option (p )
352399 return p
353400
354401
@@ -362,39 +409,42 @@ def get_parser():
362409 """Return an ArgumentParser for the 'cmdeploy' CLI"""
363410
364411 parser = argparse .ArgumentParser (description = description .strip ())
412+ parser .set_defaults (func = None , inipath = None )
365413 subparsers = parser .add_subparsers (title = "subcommands" )
366414
367415 # find all subcommands in the module namespace
368416 glob = globals ()
369417 for name , func in glob .items ():
370418 if name .endswith ("_cmd" ):
371- subparser = add_subcommand (subparsers , func )
419+ needs_config = not name .startswith ("lxc_" )
420+ subparser = add_subcommand (subparsers , func , add_config = needs_config )
372421 addopts = glob .get (name + "_options" )
373422 if addopts is not None :
374423 addopts (subparser )
375424
376425 return parser
377426
378427
379- def get_sshexec (ssh_host : str , verbose = True ):
428+ def get_sshexec (ssh_host : str , verbose = True , ssh_config = None ):
380429 if ssh_host in ["localhost" , "@local" ]:
381430 return LocalExec (verbose , docker = False )
382431 elif ssh_host == "@docker" :
383432 return LocalExec (verbose , docker = True )
384433 if verbose :
385434 print (f"[ssh] login to { ssh_host } " )
386- return SSHExec (ssh_host , verbose = verbose )
435+ return SSHExec (ssh_host , verbose = verbose , ssh_config = ssh_config )
387436
388437
389438def main (args = None ):
390439 """Provide main entry point for 'cmdeploy' CLI invocation."""
391440 parser = get_parser ()
392441 args = parser .parse_args (args = args )
393- if not hasattr ( args , " func" ) :
442+ if args . func is None :
394443 return parser .parse_args (["-h" ])
395444
396445 out = Out ()
397446 kwargs = {}
447+
398448 if args .func .__name__ not in ("init_cmd" , "fmt_cmd" ):
399449 if not args .inipath .exists ():
400450 out .red (f"expecting { args .inipath } to exist, run init first?" )
0 commit comments