An interface for executing scripts locally, or remotely over SSH
'''
    SSH Script Dashboard
    An interface for executing scripts locally, or remotely over SSH
    Copyright (C) 2023 Ivan Žalac
    https://github.com/izalac/ssh-script-dashboard

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''

from flask import Flask, render_template, session
import os
import json
import envexecute as ex


app = Flask(__name__)
version='V1.0.2'


if os.environ['AUTH_MODEL'] == 'oidc':
    from flask_pyoidc.provider_configuration import ClientMetadata
    from flask_pyoidc.provider_configuration import ProviderConfiguration
    from flask_pyoidc.user_session import UserSession
    from flask_pyoidc import OIDCAuthentication
    app.config.update(OIDC_REDIRECT_URI=os.environ['LOCAL_OIDC_REDIRECT_URI'],
                      SECRET_KEY=os.environ['LOCAL_SECRET_KEY'])
    cli_meta = ClientMetadata(client_id=os.environ['OIDC_CLIENT_ID'],
                              client_secret=os.environ['OIDC_CLIENT_SECRET'])
    provider_config = ProviderConfiguration(issuer=os.environ['OIDC_ISSUER'],
                                            client_metadata=cli_meta)
    auth = OIDCAuthentication({'oidc-provider': provider_config})
    auth.init_app(app)


# Loads the configured commands
try:
    with open("config/commands.json") as command_file:
        commands = json.load(command_file)
except Exception as e:
    message=(f'Critical error in loading commands.json: {e}')
    app.logger.critical(message)
    raise Exception(message)


# Main landing page
# If using OIDC, add the following line under the @app.route line:
# @auth.oidc_auth('oidc-provider')
@app.route("/")
def index():
    return render_template('index.html', commands=commands, version=version)


# Script running endpoint
# If using OIDC, add the following line under the @app.route line:
# @auth.oidc_auth('oidc-provider')
@app.route("/scripts/<string:script>")
def run_script(script):
    try:
        name='user'
        if os.environ['AUTH_MODEL'] == 'oidc':
            user_session = UserSession(session)
            name = user_session.userinfo['name']
        app.logger.info(f'{script} triggered by {name}')
        result = ex.default_execute(commands[script])
        return result
    except Exception as e:
        message=(f'Execution error: {e}')
        app.logger.error(message)
        return (f'{message} <br />')


# Runs the app
if __name__ == "__main__":
    app.run()

app.py
izalac/ssh-script-dashboard
'''
    SSH Script Dashboard
    An interface for executing scripts locally, or remotely over SSH
    Copyright (C) 2023 Ivan Žalac
    https://github.com/izalac/ssh-script-dashboard

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''

import fabric
import os
import json
import logging


# Tries to read EXECUTE_MODEL from environment, if it doesn't succeed, it
# populates the environment with values from config/env.json
try:
    if os.environ['EXECUTE_MODEL']:
        pass
except KeyError:
    try:
        with open('config/env.json') as env_file:
            env_data = json.load(env_file)
            for env_item in env_data:
                os.environ[env_item] = env_data[env_item]
    except Exception as e:
        message=(f'Environment load error: {e}')
        raise Exception(message)


# Sets up logging
try:
    if os.environ['LOG_FILE']:
        logging.basicConfig(filename=os.environ['LOG_FILE'],
                            filemode='a',
                            format='%(asctime)s - %(message)s',
                            level=logging.INFO)
        logging.info(f'App startup. Logging to '
                     f'{os.environ["LOG_FILE"]} started.')
except KeyError:
    logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
    logging.info(f'App startup. Logging to terminal only.')


# remote ssh server setup, if the appropriate EXECUTE_MODEL is set
if (os.environ['EXECUTE_MODEL'] == 'remote'
        or os.environ['EXECUTE_MODEL'] == 'remote-background'):
    try:
        server = fabric.Connection(os.environ['REMOTESERVER'], port=22,
                                   user=os.environ['REMOTEUSER'],
                                   connect_kwargs={"key_filename":
                                                   os.environ['REMOTECERT']})
        logging.debug('Remote server connection step successful.')
    except Exception as e:
        message=(f"Remote connection to "
                f"{os.environ['REMOTESERVER']} failed: {e}")
        logging.critical(message)
        raise Exception(message)


def local_execute(script):
    ''' Function that executes the argument on a local system, and returns
     its output in html form '''
    try:
        execute = os.popen(script)
        output = execute.read()
        logging.debug(output)
        # reformatting newlines to html <br /> tag for html output
        output = output.replace('\n', '<br />')
        return output
    except Exception as e:
        exec_err = f"Error in running script {script}: {e}"
        logging.error(exec_err)
        exec_err = f'{exec_err} <br />'
        return exec_err


def local_execute_background(script):
    ''' Function that executes the argument on a local system in background
      using tmux '''
    try:
        script_2 = f"tmux new -d '{script}'"
        os.system(script_2)
        return "Script executed in background. <br />"
    except Exception as e:
        exec_err = f"Error in running script {script}: {e}"
        logging.error(exec_err)
        exec_err = f'{exec_err} <br />'
        return exec_err


def remote_execute(script):
    ''' Function that executes the argument on a remote system, and returns
      its output in html form '''
    try:
        execute = server.run(script)
        output = execute.stdout
        logging.debug(output)
        # reformatting newlines to html <br /> tag for html output
        output = output.replace('\n', '<br />')
        return output
    except Exception as e:
        exec_err = f"Error in running script {script}: {e}"
        logging.error(exec_err)
        exec_err = f'{exec_err} <br />'
        return exec_err


def remote_execute_background(script):
    ''' Function that executes the argument on a remote system in background
      using tmux '''
    try:
        script_2 = f"tmux new -d '{script}'"
        server.run(script_2)
        return "Script executed in background. <br />"
    except Exception as e:
        exec_err = f"Error in running script {script}: {e}"
        logging.error(exec_err)
        exec_err = f'{exec_err} <br />'
        return exec_err


def default_execute(script):
    ''' Function that will call the appropriate function according to the
      EXECUTE_MODEL env variable '''
    logging.debug(f'Running {script} in {os.environ["EXECUTE_MODEL"]} mode...')
    if os.environ['EXECUTE_MODEL'] == 'local':
        return local_execute(script)
    elif os.environ['EXECUTE_MODEL'] == 'local-background':
        return local_execute_background(script)
    elif os.environ['EXECUTE_MODEL'] == 'remote':
        return remote_execute(script)
    elif os.environ['EXECUTE_MODEL'] == 'remote-background':
        return remote_execute_background(script)
    else:
        # Fallback mode
        logging.warning(f'Unknown EXECUTE_MODEL {os.environ["EXECUTE_MODEL"]},'
                      f' attempting to run {script} in local mode...')
        return local_execute(script)

envexecute.py
izalac/ssh-script-dashboard
import unittest


class DemoTest(unittest.TestCase):
    def test_demo(self):
        ''' Control unit test '''
        self.assertEqual(1, 1)

testunit.py
izalac/ssh-script-dashboard