Skip to content

Commit 8a3ef5b

Browse files
authored
Fixes #38: allow deletion/cancellation of jobs (#39)
* api: Shell2HttpAPI.delete method * tests: TestDeletion testcase * example: add deletion.py * docs: info about deletion.py
1 parent 3374da2 commit 8a3ef5b

File tree

6 files changed

+114
-18
lines changed

6 files changed

+114
-18
lines changed

README.md

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ _For urgent issues and priority support, visit [https://xscode.com/eshaan7/flask
99
[![codecov](https://codecov.io/gh/Eshaan7/Flask-Shell2HTTP/branch/master/graph/badge.svg?token=UQ43PYQPMR)](https://codecov.io/gh/Eshaan7/flask-shell2http/)
1010
[![CodeFactor](https://www.codefactor.io/repository/github/eshaan7/flask-shell2http/badge)](https://www.codefactor.io/repository/github/eshaan7/flask-shell2http)
1111
<a href="https://lgtm.com/projects/g/Eshaan7/Flask-Shell2HTTP/context:python">
12-
<img alt="Language grade: Python" src="https://img.shields.io/lgtm/grade/python/g/Eshaan7/Flask-Shell2HTTP.svg?logo=lgtm&logoWidth=18"/>
12+
<img alt="Language grade: Python" src="https://img.shields.io/lgtm/grade/python/g/Eshaan7/Flask-Shell2HTTP.svg?logo=lgtm&logoWidth=18"/>
1313
</a>
1414

1515
A minimalist [Flask](https://github.com/pallets/flask) extension that serves as a RESTful/HTTP wrapper for python's subprocess API.
@@ -18,23 +18,20 @@ A minimalist [Flask](https://github.com/pallets/flask) extension that serves as
1818
- Execute pre-defined shell commands asynchronously and securely via flask's endpoints with dynamic arguments, file upload, callback function capabilities.
1919
- Designed for binary to binary/HTTP communication, development, prototyping, remote control and [more](https://flask-shell2http.readthedocs.io/en/stable/Examples.html).
2020

21-
2221
## Use Cases
2322

2423
- Set a script that runs on a succesful POST request to an endpoint of your choice. See [Example code](examples/run_script.py).
2524
- Map a base command to an endpoint and pass dynamic arguments to it. See [Example code](examples/basic.py).
2625
- Can also process multiple uploaded files in one command. See [Example code](examples/multiple_files.py).
2726
- This is useful for internal docker-to-docker communications if you have different binaries distributed in micro-containers. See [real-life example](https://github.com/intelowlproject/IntelOwl/blob/master/integrations/static_analyzers/app.py).
28-
- You can define a callback function/ use signals to listen for process completion. See [Example code](examples/with_callback.py).
29-
* Maybe want to pass some additional context to the callback function ?
30-
* Maybe intercept on completion and update the result ? See [Example code](examples/custom_save_fn.py)
27+
- You can define a callback function/ use signals to listen for process completion. See [Example code](examples/with_callback.py).
28+
- Maybe want to pass some additional context to the callback function ?
29+
- Maybe intercept on completion and update the result ? See [Example code](examples/custom_save_fn.py)
3130
- You can also apply [View Decorators](https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/) to the exposed endpoint. See [Example code](examples/with_decorators.py).
32-
- Currently, all commands run asynchronously (default timeout is 3600 seconds), so result is not available directly. An option _may_ be provided for this in future releases for commands that return immediately.
3331

3432
> Note: This extension is primarily meant for executing long-running
3533
> shell commands/scripts (like nmap, code-analysis' tools) in background from an HTTP request and getting the result at a later time.
3634
37-
3835
## Documentation
3936

4037
[![Documentation Status](https://readthedocs.org/projects/flask-shell2http/badge/?version=latest)](https://flask-shell2http.readthedocs.io/en/latest/?badge=latest)
@@ -43,14 +40,13 @@ A minimalist [Flask](https://github.com/pallets/flask) extension that serves as
4340
- I also highly recommend the [Examples](https://flask-shell2http.readthedocs.io/en/stable/Examples.html) section.
4441
- [CHANGELOG](https://github.com/eshaan7/Flask-Shell2HTTP/blob/master/.github/CHANGELOG.md).
4542

46-
4743
## Quick Start
4844

4945
##### Dependencies
5046

51-
* Python: `>=v3.6`
52-
* [Flask](https://pypi.org/project/Flask/)
53-
* [Flask-Executor](https://pypi.org/project/Flask-Executor)
47+
- Python: `>=v3.6`
48+
- [Flask](https://pypi.org/project/Flask/)
49+
- [Flask-Executor](https://pypi.org/project/Flask-Executor)
5450

5551
##### Installation
5652

@@ -109,9 +105,9 @@ returns JSON,
109105

110106
```json
111107
{
112-
"key": "ddbe0a94",
113-
"result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94&wait=false",
114-
"status": "running"
108+
"key": "ddbe0a94",
109+
"result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94&wait=false",
110+
"status": "running"
115111
}
116112
```
117113

@@ -131,11 +127,10 @@ Returns result in JSON,
131127
"end_time": 1593019807.782958,
132128
"process_time": 0.00748753547668457,
133129
"returncode": 0,
134-
"error": null,
130+
"error": null
135131
}
136132
```
137133

138-
139134
## Inspiration
140135

141136
This was initially made to integrate various command-line tools easily with [Intel Owl](https://github.com/intelowlproject/IntelOwl), which I am working on as part of Google Summer of Code.

docs/source/Examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ I have created some example python scripts to demonstrate various use-cases. The
99
- [with_signals.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/with_signals.py): Using [Flask Signals](https://flask.palletsprojects.com/en/1.1.x/signals/) as callback function.
1010
- [with_decorators.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/with_decorators.py): Shows how to apply [View Decorators](https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/) to the exposed endpoint. Useful in case you wish to apply authentication, caching, etc. to the endpoint.
1111
- [custom_save_fn.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/custom_save_fn.py): There may be cases where the process doesn't print result to standard output but to a file/database. This example shows how to pass additional context to the callback function, intercept the future object after completion and update it's result attribute before it's ready to be consumed.
12+
- [deletion.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/deletion.py): Example demonstrating how to request cancellation/deletion of an already running job.

docs/source/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ A minimalist Flask_ extension that serves as a RESTful/HTTP wrapper for python's
2323
- This is useful for internal docker-to-docker communications if you have different binaries distributed in micro-containers.
2424
- You can define a callback function/ use signals to listen for process completion.
2525
- You can also apply View Decorators to the exposed endpoint.
26-
- Currently, all commands run asynchronously (default timeout is 3600 seconds), so result is not available directly. An option _may_ be provided for this in future release.
2726

2827
`Note: This extension is primarily meant for executing long-running
2928
shell commands/scripts (like nmap, code-analysis' tools) in background from an HTTP request and getting the result at a later time.`

examples/deletion.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# web imports
2+
from flask import Flask
3+
from flask_executor import Executor
4+
from flask_shell2http import Shell2HTTP
5+
6+
# Flask application instance
7+
app = Flask(__name__)
8+
9+
# application factory
10+
executor = Executor(app)
11+
shell2http = Shell2HTTP(app, executor)
12+
13+
14+
shell2http.register_command(
15+
endpoint="sleep",
16+
command_name="sleep",
17+
)
18+
19+
20+
# Test Runner
21+
if __name__ == "__main__":
22+
app.testing = True
23+
c = app.test_client()
24+
# request new process
25+
r1 = c.post("/sleep", json={"args": ["10"], "force_unique_key": True})
26+
print(r1)
27+
# request cancellation
28+
r2 = c.delete(f"/sleep?key={r1.get_json()['key']}")
29+
print(r2)

flask_shell2http/api.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
class Shell2HttpAPI(MethodView):
3030
"""
31-
``Flask.MethodView`` that registers ``GET`` and ``POST``
31+
``Flask.MethodView`` that creates ``GET``, ``POST`` and ``DELETE``
3232
methods for a given endpoint.
3333
This is invoked on ``Shell2HTTP.register_command``.
3434
@@ -37,6 +37,7 @@ class Shell2HttpAPI(MethodView):
3737

3838
def get(self):
3939
"""
40+
Get report by job key.
4041
Args:
4142
key (str):
4243
- Future key
@@ -138,6 +139,41 @@ def post(self):
138139
response_dict["result_url"] = self.__build_result_url(key)
139140
return make_response(jsonify(response_dict), HTTPStatus.BAD_REQUEST)
140141

142+
def delete(self):
143+
"""
144+
Cancel (if running) and delete job by job key.
145+
Args:
146+
key (str):
147+
- Future key
148+
"""
149+
try:
150+
key = request.args.get("key")
151+
logger.info(
152+
f"Job: '{key}' --> deletion requested. "
153+
f"Requester: '{request.remote_addr}'."
154+
)
155+
if not key:
156+
raise Exception("No key provided in arguments.")
157+
158+
# get the future object
159+
future: Future = self.executor.futures._futures.get(key)
160+
if not future:
161+
raise JobNotFoundException(f"No job exists for key: '{key}'.")
162+
163+
# cancel and delete from memory
164+
future.cancel()
165+
self.executor.futures.pop(key)
166+
167+
return make_response({}, HTTPStatus.NO_CONTENT)
168+
169+
except JobNotFoundException as e:
170+
logger.error(e)
171+
return make_response(jsonify(error=str(e)), HTTPStatus.NOT_FOUND)
172+
173+
except Exception as e:
174+
logger.error(e)
175+
return make_response(jsonify(error=str(e)), HTTPStatus.BAD_REQUEST)
176+
141177
@classmethod
142178
def __build_result_url(cls, key: str) -> str:
143179
return f"{request.base_url}?key={key}&wait=false"

tests/test_deletion.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from examples.deletion import app
2+
3+
from tests._utils import CustomTestCase
4+
5+
6+
class TestDeletion(CustomTestCase):
7+
uri = "/sleep"
8+
9+
def create_app(self):
10+
app.config["TESTING"] = True
11+
return app
12+
13+
def test_delete__204(self):
14+
# create command process
15+
r1 = self.client.post(self.uri, json={"args": ["10"], "force_unique_key": True})
16+
r1_json = r1.get_json()
17+
self.assertStatus(r1, 202)
18+
# request cancellation: correct key
19+
r2 = self.client.delete(f"{self.uri}?key={r1_json['key']}")
20+
self.assertStatus(r2, 204)
21+
22+
def test_delete__400(self):
23+
# create command process
24+
r1 = self.client.post(self.uri, json={"args": ["10"], "force_unique_key": True})
25+
self.assertStatus(r1, 202)
26+
# request cancellation: no key
27+
r2 = self.client.delete(f"{self.uri}?key=")
28+
self.assertStatus(r2, 400)
29+
30+
def test_delete__404(self):
31+
# create command process
32+
r1 = self.client.post(self.uri, json={"args": ["10"], "force_unique_key": True})
33+
self.assertStatus(r1, 202)
34+
# request cancellation: invalid key
35+
r2 = self.client.delete(f"{self.uri}?key=abcdefg")
36+
self.assertStatus(r2, 404)

0 commit comments

Comments
 (0)