-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[sonic-py-common] Add getstatusoutput_noshell() functions to general module #12065
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 8 commits
6a6dd70
f7d90d0
0e8f4ce
8e3b376
5b256df
bfd3862
38ca258
fbe43bf
99ccf28
cd007de
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 | ||||
|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||
| import sys | ||||||
| from subprocess import Popen, STDOUT, PIPE, CalledProcessError, check_output | ||||||
|
|
||||||
|
|
||||||
| def load_module_from_source(module_name, file_path): | ||||||
|
|
@@ -23,3 +24,90 @@ def load_module_from_source(module_name, file_path): | |||||
| sys.modules[module_name] = module | ||||||
|
|
||||||
| return module | ||||||
|
|
||||||
|
|
||||||
| def getstatusoutput_noshell(cmd): | ||||||
| """ | ||||||
| This function implements getstatusoutput API from subprocess module | ||||||
| but using shell=False to prevent shell injection. | ||||||
| Ref: https://github.com/python/cpython/blob/3.10/Lib/subprocess.py#L602 | ||||||
| """ | ||||||
| try: | ||||||
| output = check_output(cmd, universal_newlines=True, stderr=STDOUT) | ||||||
| exitcode = 0 | ||||||
| except CalledProcessError as ex: | ||||||
| output = ex.output | ||||||
| exitcode = ex.returncode | ||||||
| if output[-1:] == '\n': | ||||||
| output = output[:-1] | ||||||
| return exitcode, output | ||||||
|
|
||||||
|
|
||||||
| def getstatusoutput_noshell_pipes(*args): | ||||||
| """ | ||||||
| This function implements getstatusoutput API from subprocess module | ||||||
| but using shell=False to prevent shell injection. Input command | ||||||
| includes two or more pipe commands. | ||||||
| """ | ||||||
| if len(args) < 2: | ||||||
| raise ValueError("Need at least 2 processes") | ||||||
| # Set up more arguments in every processes | ||||||
| for arg in args: | ||||||
| arg["stdout"] = PIPE | ||||||
| arg["universal_newlines"] = True | ||||||
| arg["shell"] = False | ||||||
|
|
||||||
| # Runs all subprocesses connecting stdins and previous arguments | ||||||
| # to create the pipeline. Closes stdouts to avoid deadlocks. | ||||||
| # Ref: https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline | ||||||
| popens = [Popen(**args[0])] | ||||||
| for i in range(1,len(args)): | ||||||
| args[i]["stdin"] = popens[i-1].stdout | ||||||
| popens.append(Popen(**args[i])) | ||||||
| popens[i-1].stdout.close() | ||||||
| output = popens[-1].communicate()[0] | ||||||
| if output[-1:] == '\n': | ||||||
| output = output[:-1] | ||||||
|
|
||||||
| # Wait for the processes to terminate and return the exitcodes | ||||||
| exitcodes = [0] * len(popens) | ||||||
| while popens: | ||||||
| last = popens.pop(-1) | ||||||
| exitcodes[len(popens)] = last.wait() | ||||||
| return (exitcodes, output) | ||||||
|
|
||||||
|
|
||||||
| def check_output_pipes(*args): | ||||||
|
||||||
| def check_output_pipes(*args): | |
| def check_output_pipes(cmd0, *args): |
If you will treat the first element of args differently, it's better to define it as normal parameter. I guess your implementation will also works for only 1 cmd0, then you do not need to check len(args) at all. #Closed
Outdated
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.
Outdated
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.
pipe command -> commands connected by shell pipe(s) #Closed
Outdated
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.
Outdated
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.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||
| import sys | ||||||
| import pytest | ||||||
| import subprocess | ||||||
| from sonic_py_common.general import getstatusoutput_noshell, getstatusoutput_noshell_pipes, check_output_pipes | ||||||
|
|
||||||
|
|
||||||
| def test_getstatusoutput_noshell(tmp_path): | ||||||
| exitcode, output = getstatusoutput_noshell(['echo', 'sonic']) | ||||||
| assert (exitcode, output) == (0, 'sonic') | ||||||
|
|
||||||
| exitcode, output = getstatusoutput_noshell([sys.executable, "-c", "import sys; sys.exit(6)"]) | ||||||
| assert exitcode != 0 | ||||||
|
|
||||||
| def test_getstatusoutput_noshell_pipes(): | ||||||
| exitcode, output = getstatusoutput_noshell_pipes(dict(args=['echo', 'sonic']), dict(args=['awk', '{print $1}'])) | ||||||
|
||||||
| exitcode, output = getstatusoutput_noshell_pipes(dict(args=['echo', 'sonic']), dict(args=['awk', '{print $1}'])) | |
| exitcode, output = getstatusoutput_noshell_pipes(['echo', 'sonic'], ['awk', '{print $1}']) |
You can still call the function in traditional way if function parameter is *args.
#Closed
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.
Please add unit tests to cover new functions. #Closed
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.
Sure, working on UT