Author avatar

Red Moses

Running Shell Commands with Flask

Red Moses

  • Jan 10, 2019
  • 8 Min read
  • 88,549 Views
  • Jan 10, 2019
  • 8 Min read
  • 88,549 Views
Python

Introduction

Flask is an amazingly lightweight framework, and in my opinion it's a great option for writing simple applications in Python.

Background

An application in my company performs some tasks and then updates a MySql database accordingly. So to know how the application is progressing with the day's tasks, one has to use SSH (Secure Shell protocol) to get into the application server and run the appropriate queries using MySQL client. This is because the application only starts creating reports once all tasks are finished.

My objective was to figure out a way for someone without system administration knowledge to access this data securely without additional assistance.

Here we go...

My plan was to create a flask application that would be accessible from any web browser. In this application, when the correct path is accessed, the software will run some predefined shell command and return the results to the browser. Now this output might be a sensitive information, so we just can't allow anyone to be able to access this. To implement a basic layer of security I have used IP whitelisting, or only allowing certain IPs to acccess the application.

I ran the below commands on Ubuntu 14.04 Trusty, if you are using a different OS then your commands may vary accordingly.

Prerequisites

To go along with this tutorial you must have the following installed in your system.

  • Python3 (usually installed by default on Ubuntu 14.04)
  • virtualenv (sudo apt-get install python-virtualenv)
  • MySql Server 5.5+ & Client (sudo apt-get install mysql-server-5.5)
  • Supervisor (sudo apt-get install supervisor)

Prepare your virtual environment

Use the following commands to prepare your virtual environment (virtualenv) -

1# go to your workspace directory
2cd ~/workspace/
3# create a virtualenv using python3
4virtualenv -p /usr/bin/python3 flaskshell
5# enter the virtualenv directory and perform the basic package installations and tasks
6cd flaskshell
7# activate virtualenv
8source bin/activate
9# install flask
10pip install flask
11# create src and logs directory
12mkdir src logs
bash

Prepare a sample database

Since we will soon run a MySQL query, we need to prepare a sample database for our task.

1# create the database
2mysql -u root -p -e "CREATE DATABASE flasktest; GRANT ALL ON flasktest.* TO flaskuser@localhost IDENTIFIED BY 'flask123'; FLUSH PRIVILEGES"
3# create a sample table
4mysql -uflaskuser -pflask123 -e "CREATE TABLE flasktest.tasks (task_id INT NOT NULL AUTO_INCREMENT, task_title VARCHAR(50), task_status VARCHAR(50), PRIMARY KEY (task_id));"
5# insert some sample data
6mysql -uflaskuser -pflask123 -e "INSERT INTO flasktest.tasks (task_title, task_status) VALUES ('Task 1', 'Success');"
7mysql -uflaskuser -pflask123 -e "INSERT INTO flasktest.tasks (task_title, task_status) VALUES ('Task 2', 'Pending');"
8mysql -uflaskuser -pflask123 -e "INSERT INTO flasktest.tasks (task_title, task_status) VALUES ('Task 3', 'Failed');"
bash

Let's check whether the database and table creations went according to plan.

1mysql -uflaskuser -pflask123 -e "USE flasktest; SELECT COUNT(*) FROM tasks WHERE task_status='Success';"
2
3# the above command should print this,
4
5+----------+
6| COUNT(*) |
7+----------+
8| 1 |
9+----------+
bash

You may run the commands with statements for 'Pending' and 'Failed' as well just to double-check. All the queries should return the same result.

The code

Create a file called app.py inside the src directory,

1touch ~/workspace/flaskshell/src/app.py
bash

Inside the file put in the code listed below.

1from flask import Flask
2from flask import request
3import subprocess
4
5
6app = Flask('flaskshell')
7ip_whitelist = ['192.168.1.2', '192.168.1.3']
8query_success = "SELECT COUNT(*) FROM flasktest.tasks WHERE task_status='Success'"
9query_pending = "SELECT COUNT(*) FROM flasktest.tasks WHERE task_status='Pending'"
10query_failed = "SELECT COUNT(*) FROM flasktest.tasks WHERE task_status='Failed'"
11
12
13def valid_ip():
14    client = request.remote_addr
15    if client in ip_whitelist:
16        return True
17    else:
18        return False
19
20
21@app.route('/status/')
22def get_status():
23    if valid_ip():
24        command_success = "mysql -uflaskuser -pflask123 -e '{0}'".format(
25            query_success)
26        command_pending = "mysql -uflaskuser -pflask123 -e '{0}'".format(
27            query_pending)
28        command_failed = "mysql -uflaskuser -pflask123 -e '{0}'".format(
29            query_failed)
30
31        try:
32            result_success = subprocess.check_output(
33                [command_success], shell=True)
34            result_pending = subprocess.check_output(
35                [command_pending], shell=True)
36            result_failed = subprocess.check_output(
37                [command_failed], shell=True)
38        except subprocess.CalledProcessError as e:
39            return "An error occurred while trying to fetch task status updates."
40
41        return 'Success %s, Pending %s, Failed %s' % (result_success, result_pending, result_failed)
42    else:
43        return """<title>404 Not Found</title>
44               <h1>Not Found</h1>
45               <p>The requested URL was not found on the server.
46               If you entered the URL manually please check your
47               spelling and try again.</p>""", 404
48
49
50if __name__ == '__main__':
51    app.run()
python

Line 7 >> This is the array for the whitelisted IPs. You should replace the IPs as needed. You may put in virtually as many ips as you want in this array

Lines 8-10 >> I'm defining the queries here. You may change the query to suit your needs.

Lines 13-18 >> The valid_ip() method returns true if the client's IP belongs in the white list, otherwise it returns false. It gets the client's IP using the request package from Flask. This request package is defined on line 2

Line 21 >> Defines the route for accessing the application

Line 23 >> Before processing the request check if the client's IP belongs to the white list. If it does not, show Flask's default 404 page (lines 43-47)

Lines 24-29 >> Compose the shell commands using the queries defined earlier.

Lines 32-37 >> Try running the shell commands. The application will either throw an error or, if execution is successful, it will return the results (line 41)

Run the application as a service

To run the application as a service I used Supervisor. This is a matter of personal preference; feel free to use any other process control system.

Define a program on Supervisor.

Create a new Supervisor config file.

1sudo vim /etc/supervisor/conf.d/flaskshell.conf
bash

Copy and paste the following code into the file. At this point, you must put the app in - /home/user/workspace/flaskshell.

1[program:stats]
2directory = /home/user/workspace/flaskshell/src
3command = /home/user/workspace/flaskshell/bin/python app.py
4redirect_stderr = true
5stdout_logfile = /home/user/workspace/flaskshell/logs/out.log
6stderr_logfile = /home/user/workspace/flaskshell/logs/error.log
bash

Now you should update the Supervisor config and start the application.

1sudo supervisorctl update stats
2sudo supervisorctl start stats
bash

Accessing the application

Since we have not defined any port for the application, it will default to port 5000. To change this, follow the instructions I found on this Stack Overflow page.

You can find the application at http://SERVER_IP:5000. And the status updates should be available at http://SERVER_IP:5000/status/.

I hope my post was enjoyable and of help to you. Please feel free to leave any comments below with thoughts and feedback.