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 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 string representation of the LocalGroup.
295+ """
296+ return self .name
297+
279298 def add (
280299 self ,
281300 * ,
@@ -421,3 +440,174 @@ 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 string representation of the LocalSudoRule.
465+ """
466+ if self .rule_str :
467+ return self .rule_str
468+ else :
469+ return self .name
470+
471+ @staticmethod
472+ def _format_list (item : str | Any | list [str | Any ], add_percent : bool = False ) -> str :
473+ """
474+ Format the item as a string.
475+
476+ :param item: object to be formatted
477+ :type item: str | Any| list[str | Any]
478+ :param add_percent: If true, prepend % to the item, defaults to False
479+ :type add_percent: bool, optional
480+ :return: Formatted string.
481+ :rtype: str
482+ """
483+ if isinstance (item , list ):
484+ result = ", " .join (f"%{ str (x )} " if isinstance (x , LocalGroup ) and add_percent else str (x ) for x in item )
485+ else :
486+ if isinstance (item , LocalGroup ) and add_percent :
487+ result = f"%{ str (item )} "
488+ else :
489+ result = str (item )
490+ return result
491+
492+ def add (
493+ self ,
494+ * ,
495+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] = "ALL" ,
496+ host : str | list [str ] = "ALL" ,
497+ command : str | list [str ] = "ALL" ,
498+ option : str | list [str ] | None = None ,
499+ runasuser : str | LocalUser | list [str | LocalUser ] | None = None ,
500+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
501+ order : int | None = None ,
502+ nopasswd : bool | None = None ,
503+ ) -> LocalSudoRule :
504+ """
505+ Create new sudo rule.
506+
507+ :param user: sudoUser attribute, defaults to ALL
508+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup]
509+ :param host: sudoHost attribute, defaults to ALL
510+ :type host: str | list[str],
511+ :param command: sudoCommand attribute, defaults to ALL
512+ :type command: str | list[str],
513+ :param option: sudoOption attribute, defaults to None
514+ :type option: str | list[str] | None, optional
515+ :param runasuser: sudoRunAsUser attribute, defaults to None
516+ :type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
517+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
518+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
519+ :param order: sudoOrder attribute, defaults to None
520+ :type order: int | None, optional
521+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
522+ :type nopasswd: bool | None, optional
523+ :return: New sudo rule object.
524+ :rtype: LocalSudoRule
525+ """
526+ orderstr = f"{ order :02d} " if order is not None else str (len (self .util ._sudorules ))
527+ if self .filename is None :
528+ self .filename = f"{ orderstr } _{ self .name } "
529+
530+ # Remember arguments so we can use them in modify if needed
531+ self .__rule = dict [str , Any ](
532+ user = user ,
533+ host = host ,
534+ command = command ,
535+ option = option ,
536+ runasuser = runasuser ,
537+ runasgroup = runasgroup ,
538+ order = order ,
539+ nopasswd = nopasswd ,
540+ )
541+ run_as_str = ""
542+ if runasuser or runasgroup :
543+ run_as_str += "("
544+ if runasuser :
545+ run_as_str += LocalSudoRule ._format_list (runasuser )
546+ if runasgroup :
547+ run_as_str += f":{ LocalSudoRule ._format_list (runasgroup )} "
548+ run_as_str += ")"
549+ user_str = LocalSudoRule ._format_list (user , add_percent = True )
550+ host_str = LocalSudoRule ._format_list (host )
551+ tagspec_str = "NOPASSWD:" if nopasswd else ""
552+ command_str = LocalSudoRule ._format_list (command )
553+ rule_str = f"{ user_str } { host_str } ={ run_as_str } { tagspec_str } { command_str } \n "
554+ self .rule_str = rule_str
555+ self .util .fs .write (f"/etc/sudoers.d/{ self .filename } " , self .rule_str )
556+ self .util ._sudorules .append (self )
557+ return self
558+
559+ def modify (
560+ self ,
561+ * ,
562+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
563+ host : str | list [str ] | None = None ,
564+ command : str | list [str ] | None = None ,
565+ option : str | list [str ] | None = None ,
566+ runasuser : str | LocalUser | list [str | LocalUser ] | None = None ,
567+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
568+ order : int | None = None ,
569+ nopasswd : bool | None = None ,
570+ ) -> LocalSudoRule :
571+ """
572+ Modify existing Local sudo rule.
573+
574+ :param user: sudoUser attribute, defaults to None
575+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
576+ :param host: sudoHost attribute, defaults to None
577+ :type host: str | list[str] | None, optional
578+ :param command: sudoCommand attribute defaults to None
579+ :type command: str | list[str] | None, optional
580+ :param option: sudoOption attribute, defaults to None
581+ :type option: str | list[str] | None, optional
582+ :param runasuser: sudoRunAsUser attribute, defaults to None
583+ :type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
584+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
585+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
586+ :param order: sudoOrder attribute, defaults to None
587+ :type order: int | None, optional
588+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
589+ :type nopasswd: bool | None, optional
590+ :return: New sudo rule object.
591+ :rtype: LocalSudoRule
592+ """
593+ self .delete ()
594+ self .add (
595+ user = user if user is not None else self .__rule .get ("user" , "ALL" ),
596+ host = host if host is not None else self .__rule .get ("host" , "ALL" ),
597+ command = command if command is not None else self .__rule .get ("command" , "ALL" ),
598+ option = option if option is not None else self .__rule .get ("option" , None ),
599+ runasuser = runasuser if runasuser is not None else self .__rule .get ("runasuser" , None ),
600+ runasgroup = runasgroup if runasgroup is not None else self .__rule .get ("runasgroup" , None ),
601+ order = order if order is not None else self .__rule .get ("order" , None ),
602+ nopasswd = nopasswd if nopasswd is not None else self .__rule .get ("nopasswd" , None ),
603+ )
604+ return self
605+
606+ def delete (self ) -> None :
607+ """
608+ Delete local sudo rule.
609+ """
610+ if self .filename :
611+ self .util .fs .rm (f"/etc/sudoers.d/{ self .filename } " )
612+ if self in self .util ._sudorules :
613+ self .util ._sudorules .remove (self )
0 commit comments