Skip to content
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

Feature/shahzaib #1

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 48 additions & 21 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
def cleanup_on_exit():
"""calls the cleanup route at app shutdown to remove any spawned containers."""
with app.test_client() as flask_client:
response = flask_client.post('/cleanup') # call the /cleanup route
response = flask_client.post('/remove_all_containers') # calls remove_all_containers route
app.logger.info(f"Cleanup result: {response.get_data(as_text=True)}")

atexit.register(cleanup_on_exit) # register 'cleanup' to run when the app quits or receives SIGINT
Expand Down Expand Up @@ -207,28 +207,31 @@ def run_ansible(hosts, command):
finally:
cleanup_files([inventory_file_path, playbook_file_path])

def remove_containers(containers, type="spawned"):
def remove_or_stop_containers(containers, action):
"""
Stops and removes specified containers.
Stops or removes the specified containers based on the action parameter.
"""
for container in containers:
try:
# if container.status != 'exited' and container.status != 'created':
container.stop() # [NOTE] default timeout is culprit for unssuccessful removal in 1st attempt
container.remove()
app.logger.info(f"{type} container {container.id[:12]} removed.")
if container.status == 'running':
container.stop() # [NOTE] default timeout is culprit for unssuccessful removal in 1st attempt
if action == 'remove':
container.remove()
app.logger.info(f"spawned container {container.id[:12]} {'removed' if action == 'remove' else 'stopped'}")

except Exception as e:
app.logger.warning(f"Failed to remove {type} container {container.id[:12]}: {str(e)}")
app.logger.warning(f"Failed to {action} spawned container {container.id[:12]}: {str(e)}")


## ROUTES FOR FLASK APP ##
# HTTP status codes: 200 - OK (default), 400 - Bad Request, 404 - Not Found, 500 - Internal Server Error

@app.route('/')
def index():
def MAIN_INDEX_ROUTE():
return render_template('index.html')

@app.route('/spawn', methods=['POST'])
def spawn_machines():
def SPAWN_MACHINES_ROUTE():
try:
num_machines = request.form.get('num_machines', type=int)
if not isinstance(num_machines, int) or num_machines <= 0:
Expand All @@ -238,7 +241,7 @@ def spawn_machines():
containers = [] # Track spawned containers
machine_info = [] # Track machine details for display

for i in range(num_machines):
for _ in range(num_machines):
try:
container = spawn_container(public_key)
host_port = wait_for_container(container)
Expand Down Expand Up @@ -268,7 +271,7 @@ def spawn_machines():


@app.route('/run_command', methods=['POST'])
def run_command():
def RUN_COMMAND_ROUTE():
try:
command = request.form['command']
if not command or ';' in command or '&&' in command: # prevent command injection
Expand Down Expand Up @@ -305,21 +308,45 @@ def run_command():
app.logger.error(f"Error running command: {str(e)}")
return jsonify({"error": str(e)}), 500

@app.route('/cleanup', methods=['POST'])
def cleanup_containers():
"""
Cleans up all spawned containers and their associated data.
"""
@app.route('/<action>_all_containers', methods=['POST'])
def STOP_OR_REMOVE_ALL_CONTAINERS_ROUTE(action):
try:
# get all spawned containers ([NOTE] including orphaned?)
if not session.get('machine_info'): # safeguard against empty session data
return jsonify({"status": "No machines spawned. Nothing to stop or remove."})

if action not in ['stop', 'remove']: # validate the action parameter
return jsonify({"error": f"Invalid action: {action}. Use 'stop' or 'remove'."}), 400

containers = client.containers.list(all=True, filters={"label": "flask_app=spawned_container"})
remove_containers(containers)
remove_or_stop_containers(containers, action)

session.pop('machine_info', None) # clear session data
return jsonify({"status": "Cleanup completed successfully."})
return jsonify({"status": f"All containers {'stopped and' if action == 'stop' else ''} removed successfully."})

except Exception as e:
app.logger.error(f"Error {'stopping' if action == 'stop' else 'removing' } all containers: {str(e)}")
return jsonify({"error": str(e)}), 500


@app.route('/<action>_container', methods=['POST'])
def STOP_OR_REMOVE_CONTAINER_ROUTE():
try:
container_id = request.form.get('container_id')
if not container_id:
return jsonify({"error": "No container ID provided."}), 400

container = client.containers.get(container_id)
remove_or_stop_containers([container], 'stop')

# Remove the stopped container from the session data
session['machine_info'] = [machine for machine in session.get('machine_info', []) if machine['container_id'] != container_id]

return jsonify({"status": f"Container {container_id[:12]} stopped and removed successfully."})

except docker.errors.NotFound:
return jsonify({"error": f"Container {container_id[:12]} not found."}), 404
except Exception as e:
app.logger.error(f"Error during cleanup: {str(e)}")
app.logger.error(f"Error stopping container {container_id[:12]}: {str(e)}")
return jsonify({"error": str(e)}), 500


Expand Down
52 changes: 52 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
colors: {
primary: '#3B82F6',
secondary: '#10B981',
danger: '#EF4444',
}
}
}
Expand All @@ -35,6 +36,9 @@ <h1 class="text-4xl font-bold text-center mb-8 text-gray-800">Ansible Manager</h
<button onclick="spawnMachines()" class="bg-primary text-white font-semibold px-6 py-2 rounded-md hover:bg-blue-600 transition duration-300 ease-in-out transform hover:scale-105">
Spawn Machines
</button>
<button onclick="stopAllMachines()" class="bg-danger text-white font-semibold px-6 py-2 rounded-md hover:bg-red-600 transition duration-300 ease-in-out transform hover:scale-105">
Stop All Machines
</button>
</div>
</div>
<div id="machineInfo" class="space-y-4"></div>
Expand Down Expand Up @@ -115,6 +119,11 @@ <h3 class="text-2xl font-semibold mb-4 text-gray-800">Machine ${index + 1}</h3>
<p class="text-gray-600"><span class="font-medium text-gray-700">SSH Command:</span> <code class="bg-gray-100 px-2 py-1 rounded">${machine.host_command}</code></p>
<p class="text-gray-600"><span class="font-medium text-gray-700">SSH Status:</span> <span class="${machine.ssh_status === 'Ready' ? 'text-green-600' : 'text-red-600'} font-semibold">${machine.ssh_status}</span></p>
</div>
<div class="mt-4">
<button onclick="stopMachine('${machine.container_id}')" class="bg-danger text-white font-semibold px-4 py-2 rounded-md hover:bg-red-600 transition duration-300 ease-in-out transform hover:scale-105">
Stop Machine
</button>
</div>
`;
machineInfo.appendChild(machineDiv);
});
Expand Down Expand Up @@ -160,6 +169,49 @@ <h3 class="text-2xl font-semibold mb-4 text-gray-800">Machine ${index + 1} (${ma
commandResults.appendChild(resultDiv);
});
}

function stopAllMachines() {
const machineInfo = document.getElementById('machineInfo');
machineInfo.innerHTML = '<div class="text-center text-xl font-semibold text-gray-600">Stopping all machines...</div>';

axios.post('/remove_all_containers') // [IMPROVEMENT] change to stop_all_machines for later
.then(function (response) {
if (response.data.error) {
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-red-600">Error: ${response.data.error}</div>`;
return;
}
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-green-600">${response.data.status}</div>`;
machineInfoData = [];
})
.catch(function (error) {
console.error('Error:', error);
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-red-600">Error stopping machines: ${error.response?.data?.error || error.message}</div>`;
});
}

function stopMachine(containerId) {
const machineInfo = document.getElementById('machineInfo');
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-gray-600">Stopping machine ${containerId}...</div>`;

axios.post('/remove_container', `container_id=${containerId}`, { // [IMPROVEMENT] change to stop_container for later
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(function (response) {
if (response.data.error) {
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-red-600">Error: ${response.data.error}</div>`;
return;
}
machineInfoData = machineInfoData.filter(machine => machine.container_id !== containerId);
displayMachineInfo(machineInfoData);
machineInfo.insertAdjacentHTML('afterbegin', `<div class="text-center text-xl font-semibold text-green-600 mb-4">${response.data.status}</div>`);
})
.catch(function (error) {
console.error('Error:', error);
machineInfo.innerHTML = `<div class="text-center text-xl font-semibold text-red-600">Error stopping machine: ${error.response?.data?.error || error.message}</div>`;
});
}
</script>
</body>
</html>
Expand Down