11"Managing local users and groups."
22
33from __future__ import annotations
4+ from typing import Any
45
56import jc
67from pytest_mh import MultihostHost , MultihostUtility
1213 "LocalGroup" ,
1314 "LocalUser" ,
1415 "LocalUsersUtils" ,
16+ "LocalSudoRule" ,
1517]
1618
1719
@@ -35,6 +37,7 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem) -> None:
3537 self .fs : LinuxFileSystem = fs
3638 self ._users : list [str ] = []
3739 self ._groups : list [str ] = []
40+ self ._sudorules : list [LocalSudoRule ] = []
3841
3942 def teardown (self ) -> None :
4043 """
@@ -53,6 +56,9 @@ def teardown(self) -> None:
5356 if cmd :
5457 self .host .conn .run ("set -e\n \n " + cmd )
5558
59+ for rule in self ._sudorules :
60+ rule .delete ()
61+
5662 super ().teardown ()
5763
5864 def user (self , name : str ) -> LocalUser :
@@ -421,3 +427,157 @@ def remove_members(self, members: list[LocalUser]) -> LocalGroup:
421427 self .util .host .conn .run ("set -ex\n " + cmd , log_level = ProcessLogLevel .Error )
422428
423429 return self
430+
431+
432+ class LocalSudoRule (object ):
433+ """
434+ Local sudo rule management.
435+ """
436+
437+ def __init__ (self , util : LocalUsersUtils , name : str ) -> None :
438+ """
439+ :param util: LocalUsersUtils util object.
440+ :param name: Sudo rule name.
441+ :type name: str
442+ """
443+ self .name = name
444+ self .util = util
445+ self .__rule : dict [str , Any ] = dict ()
446+ self .filename : str | None = None
447+
448+ def add (
449+ self ,
450+ * ,
451+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
452+ host : str | list [str ] | None = None ,
453+ command : str | list [str ] | None = None ,
454+ option : str | list [str ] | None = None ,
455+ runasuser : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
456+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
457+ order : int | None = None ,
458+ nopasswd : bool | None = None ,
459+ ) -> LocalSudoRule :
460+ """
461+ Create new sudo rule.
462+
463+ :param user: sudoUser attribute, defaults to None
464+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
465+ :param host: sudoHost attribute, defaults to None
466+ :type host: str | list[str] | None, optional
467+ :param command: sudoCommand attribute, defaults to None
468+ :type command: str | list[str] | None, optional
469+ :param option: sudoOption attribute, defaults to None
470+ :type option: str | list[str] | None, optional
471+ :param runasuser: sudoRunAsUser attribute, defaults to None
472+ :type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
473+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
474+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
475+ :param order: sudoOrder attribute, defaults to None
476+ :type order: int | None, optional
477+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
478+ :type nopasswd: bool | None, optional
479+ :return: _description_
480+ :rtype: LocalSudoRule
481+ """
482+ orderstr = f"{ order :02d} " if order else str (len (self .util ._sudorules ))
483+ self .filename = f"{ orderstr } _{ self .name } "
484+ # Remember arguments so we can use them in modify if needed
485+ self .__rule = dict (
486+ user = user ,
487+ host = host ,
488+ command = command ,
489+ option = option ,
490+ runasuser = runasuser ,
491+ runasgroup = runasgroup ,
492+ order = order ,
493+ nopasswd = nopasswd ,
494+ )
495+ run_as_str = ""
496+ if runasuser or runasgroup :
497+ run_as_str += "("
498+ if runasuser :
499+ if isinstance (runasuser , list ):
500+ runasuser_str = ", " .join (runasuser )
501+ else :
502+ runasuser_str = runasuser
503+ run_as_str += f"{ runasuser_str } "
504+ if runasgroup :
505+ if isinstance (runasgroup , list ):
506+ runasgroup_str = ", " .join (runasgroup )
507+ else :
508+ runasgroup_str = runasgroup
509+ run_as_str += f":{ runasgroup_str } "
510+ run_as_str += ")"
511+ if isinstance (user , list ):
512+ user_str = ", " .join (user )
513+ else :
514+ user_str = user if user else ""
515+ if isinstance (host , list ):
516+ host_str = ", " .join (host )
517+ else :
518+ host_str = f"{ host } =" if host else "="
519+ tagspec_str = ""
520+ if nopasswd :
521+ tagspec_str = "NOPASSWD:"
522+ if isinstance (command , list ):
523+ command_str = ", " .join (command )
524+ else :
525+ command_str = command if command else ""
526+ rule_str = f"{ user_str } { host_str } { run_as_str } { tagspec_str } { command_str } \n "
527+ self .util .fs .write (f"/etc/sudoers.d/{ self .filename } " , rule_str )
528+ self .util ._sudorules .append (self )
529+ return self
530+
531+ def modify (
532+ self ,
533+ * ,
534+ user : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
535+ host : str | list [str ] | None = None ,
536+ command : str | list [str ] | None = None ,
537+ option : str | list [str ] | None = None ,
538+ runasuser : str | LocalUser | LocalGroup | list [str | LocalUser | LocalGroup ] | None = None ,
539+ runasgroup : str | LocalGroup | list [str | LocalGroup ] | None = None ,
540+ order : int | None = None ,
541+ nopasswd : bool | None = None ,
542+ ) -> LocalSudoRule :
543+ """
544+ Modify existing Local sudo rule.
545+
546+ :param user: sudoUser attribute, defaults to None
547+ :type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
548+ :param host: sudoHost attribute, defaults to None
549+ :type host: str | list[str] | None, optional
550+ :param command: sudoCommand attribute, defaults to None
551+ :type command: str | list[str] | None, optional
552+ :param option: sudoOption attribute, defaults to None
553+ :type option: str | list[str] | None, optional
554+ :param runasuser: sudoRunAsUser attribute, defaults to None
555+ :type runasuser: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
556+ :param runasgroup: sudoRunAsGroup attribute, defaults to None
557+ :type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
558+ :param order: sudoOrder attribute, defaults to None
559+ :type order: int | None, optional
560+ :param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
561+ :type nopasswd: bool | None, optional
562+ :return: _description_
563+ :rtype: LocalSudoRule
564+ """
565+ self .delete ()
566+ self .add (
567+ user = user if user is not None else self .__rule .get ("user" , None ),
568+ host = host if host is not None else self .__rule .get ("host" , None ),
569+ command = command if command is not None else self .__rule .get ("command" , None ),
570+ option = option if option is not None else self .__rule .get ("option" , None ),
571+ runasuser = runasuser if runasuser is not None else self .__rule .get ("runasuser" , None ),
572+ runasgroup = runasgroup if runasgroup is not None else self .__rule .get ("runasgroup" , None ),
573+ order = order if order is not None else self .__rule .get ("order" , None ),
574+ nopasswd = nopasswd if nopasswd is not None else self .__rule .get ("nopasswd" , None ),
575+ )
576+ return self
577+
578+ def delete (self ) -> None :
579+ """
580+ Delete local sudo rule.
581+ """
582+ self .util .fs .rm (f"/etc/sudoers.d/{ self .filename } " )
583+ self .util ._sudorules .remove (self )
0 commit comments