22
33from __future__ import annotations
44
5+ from typing import Any
6+
57import jc
68from pytest_mh import MultihostHost , MultihostUtility
79from pytest_mh .cli import CLIBuilder , CLIBuilderArgs
1214 "LocalGroup" ,
1315 "LocalUser" ,
1416 "LocalUsersUtils" ,
17+ "LocalSudoRule" ,
1518]
1619
1720
@@ -35,6 +38,7 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem) -> None:
3538 self .fs : LinuxFileSystem = fs
3639 self ._users : list [str ] = []
3740 self ._groups : list [str ] = []
41+ self ._sudorules : list [LocalSudoRule ] = []
3842
3943 def teardown (self ) -> None :
4044 """
@@ -53,6 +57,9 @@ def teardown(self) -> None:
5357 if cmd :
5458 self .host .conn .run ("set -e\n \n " + cmd )
5559
60+ for rule in self ._sudorules :
61+ rule .delete ()
62+
5663 super ().teardown ()
5764
5865 def user (self , name : str ) -> LocalUser :
@@ -129,6 +136,12 @@ def __init__(self, util: LocalUsersUtils, name: str) -> None:
129136 self .util = util
130137 self .name = name
131138
139+ def __str__ (self ):
140+ """
141+ Returns a user-friendly string representation of the LocalUser.
142+ """
143+ return self .name
144+
132145 def add (
133146 self ,
134147 * ,
@@ -276,6 +289,12 @@ def __init__(self, util: LocalUsersUtils, name: str) -> None:
276289 self .util = util
277290 self .name = name
278291
292+ def __str__ (self ):
293+ """
294+ Returns a user-friendly string representation of the LocalGroup.
295+ """
296+ return self .name
297+
279298 def add (
280299 self ,
281300 * ,
@@ -421,3 +440,170 @@ def remove_members(self, members: list[LocalUser]) -> LocalGroup:
421440 self .util .host .conn .run ("set -ex\n " + cmd , log_level = ProcessLogLevel .Error )
422441
423442 return self
443+
444+
445+ class LocalSudoRule (object ):
446+ """
447+ Local sudo rule management.
448+ """
449+
450+ def __init__ (self , util : LocalUsersUtils , name : str ) -> None :
451+ """
452+ :param util: LocalUsersUtils util object.
453+ :param name: Sudo rule name.
454+ :type name: str
455+ """
456+ self .name = name
457+ self .util = util
458+ self .__rule : dict [str , Any ] = dict ()
459+ self .filename : str | None = None
460+ self .rule_str : str | None = None
461+
462+ def __str__ (self ):
463+ """
464+ Returns a user-friendly string representation of the LocalSudoRule.
465+ """
466+ if self .rule_str :
467+ return self .rule_str
468+ else :
469+ return self .name
470+
471+ def add (
472+ self ,
473+ * ,
474+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
475+ host : str | list [str ] | None = None ,
476+ command : str | list [str ] | None = None ,
477+ option : str | list [str ] | None = None ,
478+ runasuser : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
479+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
480+ order : int | None = None ,
481+ nopasswd : bool | None = None ,
482+ ) -> LocalSudoRule :
483+ """
484+ Create new sudo rule.
485+
486+ :param user: sudoUser attribute, defaults to None
487+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
488+ :param host: sudoHost attribute, defaults to None
489+ :type host: str | list[str] | None, optional
490+ :param command: sudoCommand attribute, defaults to None
491+ :type command: str | list[str] | None, optional
492+ :param option: sudoOption attribute, defaults to None
493+ :type option: str | list[str] | None, optional
494+ :param runasuser: sudoRunAsUser attribute, defaults to None
495+ :type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
496+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
497+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
498+ :param order: sudoOrder attribute, defaults to None
499+ :type order: int | None, optional
500+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
501+ :type nopasswd: bool | None, optional
502+ :return: New sudo rule object.
503+ :rtype: LocalSudoRule
504+ """
505+ orderstr = f"{ order :02d} " if order else str (len (self .util ._sudorules ))
506+ self .filename = f"{ orderstr } _{ self .name } "
507+ # Remember arguments so we can use them in modify if needed
508+ self .__rule = dict (
509+ user = user ,
510+ host = host ,
511+ command = command ,
512+ option = option ,
513+ runasuser = runasuser ,
514+ runasgroup = runasgroup ,
515+ order = order ,
516+ nopasswd = nopasswd ,
517+ )
518+ run_as_str = ""
519+ if runasuser or runasgroup :
520+ run_as_str += "("
521+ if runasuser :
522+ if isinstance (runasuser , list ):
523+ runasuser_str = ", " .join (str (x ) for x in runasuser )
524+ else :
525+ runasuser_str = str (runasuser )
526+ run_as_str += f"{ runasuser_str } "
527+ if runasgroup :
528+ if isinstance (runasgroup , list ):
529+ runasgroup_str = ", " .join (str (x ) for x in runasgroup )
530+ else :
531+ runasgroup_str = str (runasgroup )
532+ run_as_str += f":{ runasgroup_str } "
533+ run_as_str += ")"
534+ if isinstance (user , list ):
535+ user_str = ", " .join (str (x ) for x in user )
536+ else :
537+ user_str = str (user ) if user else ""
538+ if isinstance (host , list ):
539+ host_str = ", " .join (host )
540+ else :
541+ host_str = f"{ host } =" if host else "="
542+ tagspec_str = ""
543+ if nopasswd :
544+ tagspec_str = "NOPASSWD:"
545+ if isinstance (command , list ):
546+ command_str = ", " .join (command )
547+ else :
548+ command_str = command if command else ""
549+ rule_str = f"{ user_str } { host_str } { run_as_str } { tagspec_str } { command_str } \n "
550+ self .rule_str = rule_str
551+ self .util .fs .write (f"/etc/sudoers.d/{ self .filename } " , self .rule_str )
552+ self .util ._sudorules .append (self )
553+ return self
554+
555+ def modify (
556+ self ,
557+ * ,
558+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
559+ host : str | list [str ] | None = None ,
560+ command : str | list [str ] | None = None ,
561+ option : str | list [str ] | None = None ,
562+ runasuser : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
563+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
564+ order : int | None = None ,
565+ nopasswd : bool | None = None ,
566+ ) -> LocalSudoRule :
567+ """
568+ Modify existing Local sudo rule.
569+
570+ :param user: sudoUser attribute, defaults to None
571+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
572+ :param host: sudoHost attribute, defaults to None
573+ :type host: str | list[str] | None, optional
574+ :param command: sudoCommand attribute, defaults to None
575+ :type command: str | list[str] | None, optional
576+ :param option: sudoOption attribute, defaults to None
577+ :type option: str | list[str] | None, optional
578+ :param runasuser: sudoRunAsUser attribute, defaults to None
579+ :type runasuser: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
580+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
581+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
582+ :param order: sudoOrder attribute, defaults to None
583+ :type order: int | None, optional
584+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
585+ :type nopasswd: bool | None, optional
586+ :return: New sudo rule object.
587+ :rtype: LocalSudoRule
588+ """
589+ self .delete ()
590+ self .add (
591+ user = user if user is not None else self .__rule .get ("user" , None ),
592+ host = host if host is not None else self .__rule .get ("host" , None ),
593+ command = command if command is not None else self .__rule .get ("command" , None ),
594+ option = option if option is not None else self .__rule .get ("option" , None ),
595+ runasuser = runasuser if runasuser is not None else self .__rule .get ("runasuser" , None ),
596+ runasgroup = runasgroup if runasgroup is not None else self .__rule .get ("runasgroup" , None ),
597+ order = order if order is not None else self .__rule .get ("order" , None ),
598+ nopasswd = nopasswd if nopasswd is not None else self .__rule .get ("nopasswd" , None ),
599+ )
600+ return self
601+
602+ def delete (self ) -> None :
603+ """
604+ Delete local sudo rule.
605+ """
606+ if self .filename :
607+ self .util .fs .rm (f"/etc/sudoers.d/{ self .filename } " )
608+ if self in self .util ._sudorules :
609+ self .util ._sudorules .remove (self )
0 commit comments