-
Notifications
You must be signed in to change notification settings - Fork 37
Improve policy admin client and related tools #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9162f94
28c173b
8c87f37
b3ac268
a02c8b8
f9dc03e
4ea6c79
73b0843
fbe7dde
f6d3665
c84ab95
9626955
26a35f8
d276ecd
d9e13c8
3a42e74
4ac20d3
db7d084
842a543
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,10 +49,9 @@ def call(dest: str, rpcname: str, arg: Optional[str] = None, *, input=None): | |
| """ | ||
| # pylint: disable=redefined-builtin | ||
|
|
||
| command = make_command(dest, rpcname, arg) | ||
| return subprocess.check_output( | ||
| command, **prepare_subprocess_kwds(input) | ||
| ).decode() | ||
| command = make_command(dest=dest, rpcname=rpcname, arg=arg) | ||
| kwds = prepare_subprocess_kwds(input, for_popen=False, for_check=True) | ||
| return subprocess.check_output(command, **kwds).decode() | ||
|
|
||
|
|
||
| async def call_async(dest, rpcname, arg=None, *, input=None): | ||
|
|
@@ -71,26 +70,10 @@ async def call_async(dest, rpcname, arg=None, *, input=None): | |
| """ | ||
| # pylint: disable=redefined-builtin | ||
|
|
||
| command = make_command(dest, rpcname, arg) | ||
|
|
||
| if input is None: | ||
| stdin = subprocess.DEVNULL | ||
| to_communicate = None | ||
| elif isinstance(input, bytes): | ||
| stdin = subprocess.PIPE | ||
| to_communicate = input | ||
| elif isinstance(input, str): | ||
| stdin = subprocess.PIPE | ||
| to_communicate = input.encode() | ||
| else: | ||
| # Assume this is a file | ||
| stdin = input | ||
| to_communicate = None | ||
|
|
||
| process = await asyncio.create_subprocess_exec( | ||
| *command, stdin=stdin, stdout=subprocess.PIPE | ||
| ) | ||
|
|
||
| command = make_command(dest=dest, rpcname=rpcname, arg=arg) | ||
| kwds = prepare_subprocess_kwds(input) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't set Took me quite a while to realize this causes double qrexec prompts with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| to_communicate = kwds.pop("input") | ||
| process = await asyncio.create_subprocess_exec(*command, **kwds) | ||
| stdout, _stderr = await process.communicate(to_communicate) | ||
| if process.returncode != 0: | ||
| raise subprocess.CalledProcessError(process.returncode, command) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,41 +34,115 @@ | |
| """ | ||
|
|
||
| from typing import List, Tuple | ||
| from warnings import warn | ||
| from subprocess import CalledProcessError | ||
|
|
||
| from ..client import call | ||
|
|
||
| # The PolicyAdmin*Exception is used by issubclass(..., PolicyAdminException). | ||
| # pylint: disable=unused-import | ||
| from .admin import ( | ||
| PolicyAdminException, | ||
| PolicyAdminTokenException, | ||
| PolicyAdminFileNotFoundException, | ||
| PolicyAdminProtocolException, | ||
| PolicyAdminSyntaxException, | ||
| PolicyAdminInvalidFileNameException, | ||
| PolicyAdminInvalidFilePathException, | ||
| ) | ||
|
|
||
|
|
||
| class PolicyClient: | ||
| def policy_list(self) -> List[str]: | ||
| return self.call("policy.List").rstrip("\n").split("\n") | ||
| def policy_list(self, is_include: bool = False) -> List[str]: | ||
| rpc = "List" | ||
| return self.call(rpc, is_include=is_include).rstrip("\n").split("\n") | ||
|
|
||
| def policy_include_list(self) -> List[str]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please keep
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added them back. I tried to issue a DeprecationWarning, but I am not sure why it not emitted. PYTHONPATH=. python3import qrexec.policy.admin_client
client = qrexec.policy.admin_client
client.policy_include_list()Warning is not emitted. |
||
| return self.call("policy.include.List").rstrip("\n").split("\n") | ||
| warn( | ||
| "Method 'policy_include_list()' is deprecated, use 'policy_list()' " | ||
| "instead", | ||
| DeprecationWarning, | ||
| ) | ||
| return self.policy_list(is_include=True) | ||
|
|
||
| def policy_get(self, name: str) -> Tuple[str, str]: | ||
| token, content = self.call("policy.Get", name).split("\n", 1) | ||
| def policy_get( | ||
| self, name: str, is_include: bool = False | ||
| ) -> Tuple[str, str]: | ||
| rpc = "Get" | ||
| token, content = self.call( | ||
| service_name=rpc, arg=name, is_include=is_include | ||
| ).split("\n", 1) | ||
| return content, token | ||
|
|
||
| def policy_include_get(self, name: str) -> Tuple[str, str]: | ||
| token, content = self.call("policy.include.Get", name).split("\n", 1) | ||
| return content, token | ||
| warn( | ||
| "Method 'policy_include_get()' is deprecated, use 'policy_get()' " | ||
| "instead", | ||
| DeprecationWarning, | ||
| ) | ||
| return self.policy_get(name=name, is_include=True) | ||
|
|
||
| def policy_replace(self, name: str, content: str, token="any"): | ||
| self.call("policy.Replace", name, token + "\n" + content) | ||
| def policy_replace( | ||
| self, | ||
| name: str, | ||
| content: str, | ||
| token: str = "any", | ||
| is_include: bool = False, | ||
| ): | ||
| rpc = "Replace" | ||
| self.call( | ||
| service_name=rpc, | ||
| arg=name, | ||
| payload=token + "\n" + content, | ||
| is_include=is_include, | ||
| ) | ||
|
|
||
| def policy_include_replace(self, name: str, content: str, token="any"): | ||
| self.call("policy.include.Replace", name, token + "\n" + content) | ||
| warn( | ||
| "Method 'policy_include_replace()' is deprecated, use " | ||
| "'policy_replace()' instead", | ||
| DeprecationWarning, | ||
| ) | ||
| return self.policy_replace( | ||
| name=name, content=content, token=token, is_include=True | ||
| ) | ||
|
|
||
| def policy_remove(self, name: str, token="any"): | ||
| self.call("policy.Remove", name, token) | ||
| def policy_remove( | ||
| self, name: str, token: str = "any", is_include: bool = False | ||
| ): | ||
| rpc = "Remove" | ||
| self.call( | ||
| service_name=rpc, arg=name, payload=token, is_include=is_include | ||
| ) | ||
|
|
||
| def policy_include_remove(self, name: str, token="any"): | ||
| self.call("policy.include.Remove", name, token) | ||
| warn( | ||
| "Method 'policy_include_remove()' is deprecated, use " | ||
| "'policy_remove()' instead", | ||
| DeprecationWarning, | ||
| ) | ||
| return self.policy_remove(name=name, token=token, is_include=True) | ||
|
|
||
| def policy_get_files(self, name: str): | ||
| result = self.call("policy.GetFiles", name) | ||
| def policy_get_files(self, name: str) -> list: | ||
| rpc = "GetFiles" | ||
| result = self.call(service_name=rpc, arg=name) | ||
| return [] if result == "" else result.rstrip("\n").split("\n") | ||
|
|
||
| @staticmethod | ||
| def call(service_name, arg=None, payload=""): | ||
| return call("dom0", service_name, arg, input=payload) | ||
| def call(service_name, arg=None, payload="", is_include: bool = False): | ||
| if is_include: | ||
| service_name = "include." + service_name | ||
| service_name = "policy." + service_name | ||
| try: | ||
| return call( | ||
| dest="dom0", rpcname=service_name, arg=arg, input=payload | ||
| ) | ||
| except CalledProcessError as exc: | ||
| stderr_exc = exc.stderr.decode() | ||
| stderr_exc_type = stderr_exc.split(" ")[0] | ||
| if stderr_exc_type in globals(): | ||
| exception = globals()[stderr_exc_type] | ||
| if issubclass(exception, PolicyAdminException): | ||
| stderr_exc_msg = stderr_exc[len(exception.__name__ + " ") :] | ||
| raise exception(stderr_exc_msg) from exc | ||
| raise | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh, sorry for not noticing before, but changes in this file are very wrong...
qrexec.clientmodule is about making generic qrexec calls from python, not just for managing policies. It must not expect any specific format of stderr. Policy admin methods client should be limited to admin_client.py.And also, it's a public API, don't change it (speaking about keyword parameters rename you did).
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It checks if the first item of stderr is the same as an exception class. It is not idiomatic, but the call is made via a subprocess/
qrexec-client-vm, so, uh... do you have any ideas?How the admin-client does it:
8d5787f: "Return policy admin exceptions to client stderr". This is writing the exception to stderr. The stdout is already being used for sending the data. I don't think we can easily make it null delimited to support more fields and exceptions without making it a breaking change. Do you have any recommendations on what to do in this case?
The change was just to raise the exception. I can make it only on call method of
admin_client.py, after we solve the issue of the point above.I guess this is 1ba1552, "Fix input builtin redefintion". The downside of using a variable with the same name as a builtin is that parsing might become confusing, in some places, it will use the value of the builtin if not redefined yet, for example, leading to strange conditions. I can drop that commit, maybe we can add it to the next breaking change of qrexec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I don't think this stdout can be used here.
Stderr is fine, but
qrexec.clientmodule shouldn't parse it. Move that parsing (catching CalledProcessError) to admin_client.py - just that.I know
inputis (also) a builtin name, but it's here to stay. It mirrors thesubprocessstandard library, which also usesinput=as the argument name for the same purpose.