from flask import Blueprint, request, jsonify, current_app
from flask_socketio import disconnect
from .. import db, socketio, connections_devices
from ..models import Device
from .validations import get_device_metadata_errors, get_password_errors
from ..functions import submit_device
from ..protections import admin_required, content_json
import itertools
bp = Blueprint("device_api", __name__, url_prefix="/api")
[docs]
@bp.route("/device", methods=["GET"])
@admin_required
def get_all_devices():
devices = Device.query.all()
return jsonify(devices=[s.to_dict() for s in devices]), 200
[docs]
@bp.route("/device/<id_>", methods=["GET"])
@admin_required
def get_device(id_):
device = Device.by_id(id_)
if not device:
return jsonify(message="Device not found."), 404
return jsonify(device=device.to_dict()), 200
[docs]
@bp.route("/device", methods=["POST"])
@admin_required
@content_json(["name", "password", "ipv4"])
def create_device():
data = request.get_json()
metadata = {
"name": data["name"],
"ipv4": data["ipv4"],
}
password = data["password"]
errors = dict(itertools.chain(
get_device_metadata_errors(**metadata),
get_password_errors(password)
))
if Device.by_name(data["name"]):
errors["name"] = "Device name conflict."
if not errors:
device = submit_device(password=password, **metadata)
return jsonify(message="Device '{}' created.".format(device.name),
device=device.to_dict()), 201
else:
return jsonify(message="Device creation failed.", errors=errors), 400
[docs]
@bp.route("/device/<id_>/password", methods=["PUT"])
@admin_required
@content_json(["new_password"])
def change_password(id_): # TODO: SECURITY: Changing device password does not require old pass => ok?
device = Device.by_id(id_)
if not device:
return jsonify(message="Device not found."), 404
data = request.get_json()
errors = dict(get_password_errors(data["new_password"]))
if not errors:
device.set_password(data["new_password"])
db.session.commit()
return jsonify(message="Device password changed successfully."), 200
else:
return jsonify(message="Device password change failed.", errors=errors), 400
[docs]
@bp.route("/device/<id_>", methods=["DELETE"])
@admin_required
def delete_device(id_):
device = Device.by_id(id_)
if not device:
return jsonify(message="Device not found."), 404
db.session.delete(device)
db.session.commit()
# It is always just one sid, but lets insure against future changes
for sid in connections_devices.by_pubid.get(id_, []):
disconnect(sid, "/device")
return jsonify(message="Device '{}' has been deleted.".format(device.name)), 200
[docs]
@bp.route("/device/<id_>", methods=["PATCH"])
@admin_required
@content_json([])
def modify_device(id_):
device = Device.by_id(id_)
if not device:
return jsonify(message="Device not found."), 404
data = request.get_json()
data = {
"name": data.get("name", device.name),
"ipv4": data.get("ipv4", device.ipv4),
}
errors = dict(get_device_metadata_errors(**data))
if data["name"] != device.name and Device.by_name(data["name"]):
errors["name"] = "Device name conflict."
if not errors:
submit_device(password=None, **data, replace_device=device)
return jsonify(message="Device '{}' modified.".format(device.name)), 200
else:
return jsonify(message="Device modification failed.", errors=errors), 400
[docs]
@bp.route("/device/<id_>/disconnect", methods=["POST"])
@admin_required
def disconnect_device(id_):
"""Api endpoint to disconnect and stop the device.
Convenient when device crashes and leaves an old connection hanging, which
in turn blocks any new connection of the same device. If the device is
actually connected it gets restartad (similarly to server restart using
``/api/shutdown``, see -> deployment/systemd).
"""
device = Device.by_id(id_)
if not device:
return jsonify(message="Device not found."), 404
sids = connections_devices.by_pubid.get(id_, [])
if len(sids) == 0:
return jsonify(message="Device not connected."), 404
current_app.logger.info("Disconnect request for device '{}'.".format(device.name))
# It is always just one sid, but lets insure against future changes
for sid in sids:
socketio.emit("bye", namespace="/device", to=sid)
disconnect(sid, "/device")
return jsonify(message="Device '{}' has been disconnected.".format(device.name)), 200