def printer_create(): data = request.json if not data: return abort(400) ip = data.get("ip", None) name = data.get("name", None) if (not ip or not name or re.match( r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:?\d{0,5}$", ip) is None): return abort(400) if printers.get_printer(ip) is not None: return abort(409) hostname = network.get_avahi_hostname(ip) printer = drivers.get_printer_instance({ "hostname": hostname, "ip": ip, "name": name, "client": "octoprint", # TODO make this more generic }) printer.sniff() network_devices.upsert_network_device(ip=ip, retry_after=None, disabled=False) printers.add_printer( name=name, hostname=hostname, ip=ip, client=printer.client_name(), client_props={ "version": printer.client.version, "connected": printer.client.connected, "read_only": printer.client.read_only, }, ) return "", 201
def printer_change_connection(uuid): # TODO this has to be streamlined, octoprint sometimes cannot handle two connect commands at once try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) data = request.json if not data: return abort(make_response("", 400)) state = data.get("state", None) printer_inst = clients.get_printer_instance(printer) if state == "online": r = printer_inst.connect_printer() return (("", 204) if r else ("Cannot change printer's connection state to online", 500)) elif state == "offline": r = printer_inst.disconnect_printer() return (("", 204) if r else ("Cannot change printer's connection state to offline", 500)) else: return abort(make_response("", 400)) return "", 204
def printer_detail(ip): fields = request.args.get("fields").split(",") if request.args.get( "fields") else [] printer = printers.get_printer(ip) if printer is None: return abort(404) return jsonify(make_printer_response(printer, fields))
def printer_modify_job(uuid): # TODO allow users to pause/cancel only their own prints via printjob_id # And allow admins to pause/cancel anything # but that means creating a new tracking of current jobs on each printer # and does not handle prints issued by bypassing Karmen Hub # Alternative is to log who modified the current job into an admin-accessible eventlog # See https://trello.com/c/uiv0luZ8/142 for details try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) data = request.json if not data: return abort(make_response("", 400)) action = data.get("action", None) if not action: return abort(make_response("", 400)) printer_inst = clients.get_printer_instance(printer) try: if printer_inst.modify_current_job(action): user = get_current_user() app.logger.info( "User %s successfully issued a modification (%s) of current job on printer %s", user["uuid"], action, uuid, ) return "", 204 return abort(make_response("", 409)) except clients.utils.PrinterClientException as e: return abort(make_response(jsonify(message=str(e)), 400))
def add_printjob(**job): 'creates a printjob returning the id of the newly created job' if 'uuid' in job: raise ValueError("Cannot add a printjob with an already set UUID.") printer = get_printer(job["printer_uuid"]) if not printer: raise ValueError(f"The printer {job['printer_uuid']} does not exist.") if printer["organization_uuid"] != job["organization_uuid"]: raise ValueError(f"The printer does not belong to the organization.") if job.get("gcode_data", None) and job["gcode_data"].get("uuid", None): if job["gcode_uuid"] != job["gcode_data"]["uuid"]: raise ValueError("printjob['gcode_uuid'] does not match printjob['gcode_data']['uuid']") # FIXME: printjob should not contain organization_uuid (it is already in printer_uuid) job['uuid'] = guid.uuid4() with get_connection() as connection: cursor = connection.cursor() cursor.execute( "INSERT INTO printjobs (uuid, gcode_uuid, organization_uuid, printer_uuid, gcode_data, printer_data, user_uuid) values (%s, %s, %s, %s, %s, %s, %s) RETURNING uuid", ( job["uuid"], job["gcode_uuid"], job["organization_uuid"], job["printer_uuid"], psycopg2.extras.Json(job.get("gcode_data", None)), psycopg2.extras.Json(job.get("printer_data", None)), job.get("user_uuid", None), ), ) data = cursor.fetchone() cursor.close() return data[0]
def printer_delete(ip): printer = printers.get_printer(ip) if printer is None: return abort(404) printers.delete_printer(ip) for device in network_devices.get_network_devices(printer["ip"]): device["disabled"] = True network_devices.upsert_network_device(**device) return "", 204
def printer_delete(uuid): try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) printers.delete_printer(uuid) return "", 204
def printer_webcam_snapshot(uuid): """ Instead of direct streaming from the end-devices, we are deferring the video-like feature to the clients. This (in case of MJPEG) brings significant performance gains throughout the whole pipeline. This endpoint serves as a redis-backed (due to multithreaded production setup with uwsgi) microcache of latest captured image from any given printer. New snapshot is requested, but the old one is served to the client. In case of the first request, a 202 is returned and the client should ask again. By asking periodically for new snapshots, the client can emulate a video-like experience. Since we have no sound, this should be fine. """ try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) printer_inst = clients.get_printer_instance(printer) if printer_inst.client_info.webcam is None: return abort(make_response("", 404)) snapshot_url = printer_inst.client_info.webcam.get("snapshot") if snapshot_url is None: return abort(make_response("", 404)) # process current future if done if FUTURES_MICROCACHE.get(uuid) and FUTURES_MICROCACHE.get(uuid).done(): WEBCAM_MICROCACHE[uuid] = FUTURES_MICROCACHE[uuid].result() try: del FUTURES_MICROCACHE[uuid] except Exception: # that's ok, probably a race condition pass # issue a new future if not present if not FUTURES_MICROCACHE.get(uuid): FUTURES_MICROCACHE[uuid] = executor.submit(_get_webcam_snapshot, snapshot_url) if WEBCAM_MICROCACHE.get(uuid) is not None: response = WEBCAM_MICROCACHE.get(uuid) return ( response.content, 200, { "Content-Type": response.headers.get("content-type", "image/jpeg") }, ) # There should be a future running, if the client retries, they should # eventually get a snapshot. # We don't want to end with an error here, so the clients keep retrying return "", 202
def printer_detail(uuid): try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) fields = request.args.get("fields").split(",") if request.args.get( "fields") else [] printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) return jsonify(make_printer_response(printer, fields))
def get_printer_inst(org_uuid, uuid): validate_uuid(uuid) printer = printers.get_printer(uuid) if printer is None or printer.get("organization_uuid") != org_uuid: return abort(make_response(jsonify(message="Not found"), 404)) network_client = network_clients.get_network_client( printer["network_client_uuid"]) printer_data = dict(network_client) printer_data.update(dict(printer)) printer_inst = clients.get_printer_instance(printer_data) return printer_inst
def check_printer(printer_uuid): app.logger.debug("Checking printer %s" % printer_uuid) raw_printer = printers.get_printer(printer_uuid) # todo: The `get_printer` method should raise an exception instead of returning None if raw_printer is None: app.logger.info( "Printer was deleted after it was scheduled for update.") return raw_client = network_clients.get_network_client( raw_printer["network_client_uuid"]) printer_data = dict(raw_client) printer_data.update(raw_printer) printer = clients.get_printer_instance(printer_data) # websocket printers are not expected to change if printer.protocol in ["http", "https"]: if printer.hostname is not None: current_ip = network.get_avahi_address(printer.hostname) if current_ip is not None and current_ip != printer.ip: printer.ip = current_ip printer.update_network_base() hostname = network.get_avahi_hostname(printer.ip) if hostname is not None and hostname != printer.hostname: printer.hostname = hostname printer.update_network_base() now = datetime.now() if now.minute % 15 == 0 and printer.client_info.connected: printer.sniff() printer.karmen_sniff() else: printer.is_alive() if (printer.client_info.pill_info and printer.client_info.pill_info.get( "update_status", None) is not None): printer.karmen_sniff() if printer.hostname != raw_client.get( "hostname") or printer.ip != raw_client.get("ip"): network_clients.update_network_client( uuid=raw_client["uuid"], hostname=printer.hostname, ip=printer.ip, ) printers.update_printer( uuid=printer.uuid, client_props={ "version": printer.client_info.version, "connected": printer.client_info.connected, "access_level": printer.client_info.access_level, "api_key": printer.client_info.api_key, "webcam": printer.client_info.webcam, "plugins": printer.client_info.plugins, "pill_info": printer.client_info.pill_info, }, )
def printer_delete(org_uuid, uuid): validate_uuid(uuid) printer = printers.get_printer(uuid) if printer is None or printer.get("organization_uuid") != org_uuid: return abort(make_response(jsonify(message="Not found"), 404)) printers.delete_printer(uuid) network_client_records = printers.get_printers_by_network_client_uuid( printer["network_client_uuid"]) if len(network_client_records) == 0: network_clients.delete_network_client(printer["network_client_uuid"]) return "", 204
def printjob_create(): data = request.json if not data: return abort(make_response("", 400)) gcode_id = data.get("gcode", None) printer_uuid = data.get("printer", None) if not gcode_id or not printer_uuid: return abort(make_response("", 400)) printer = printers.get_printer(printer_uuid) if printer is None: return abort(make_response("", 404)) gcode = gcodes.get_gcode(gcode_id) if gcode is None: return abort(make_response("", 404)) try: printer_inst = clients.get_printer_instance(printer) uploaded = printer_inst.upload_and_start_job(gcode["absolute_path"], gcode["path"]) if not uploaded: return abort( make_response( jsonify(message="Cannot upload the g-code to the printer"), 500)) printjob_id = printjobs.add_printjob( gcode_id=gcode["id"], printer_uuid=printer["uuid"], user_uuid=get_current_user()["uuid"], gcode_data={ "id": gcode["id"], "filename": gcode["filename"], "size": gcode["size"], "available": True, }, printer_data={ "ip": printer["ip"], "port": printer["port"], "hostname": printer["hostname"], "name": printer["name"], "client": printer["client"], }, ) return ( jsonify({ "id": printjob_id, "user_uuid": get_current_user()["uuid"] }), 201, ) except clients.utils.PrinterClientException: return abort(make_response("", 409))
def test_patch(self): with app.test_client() as c: c.set_cookie("localhost", "access_token_cookie", TOKEN_ADMIN) response = c.patch( "/printers/%s" % self.uuid, headers={"x-csrf-token": TOKEN_ADMIN_CSRF}, json={ "name": "random-test-printer-name", "protocol": "https" }, ) self.assertEqual(response.status_code, 200) p = printers.get_printer(self.uuid) self.assertEqual(p["name"], "random-test-printer-name") self.assertEqual(p["protocol"], "https")
def test_patch_api_keychange(self, mock_session_get): with app.test_client() as c: c.set_cookie("localhost", "access_token_cookie", TOKEN_ADMIN) response = c.patch( "/printers/%s" % self.uuid, headers={"x-csrf-token": TOKEN_ADMIN_CSRF}, json={ "name": "random-test-printer-name", "api_key": "1234", }, ) self.assertEqual(response.status_code, 200) p = printers.get_printer(self.uuid) self.assertEqual(p["client_props"]["api_key"], "1234") self.assertEqual(mock_session_get.call_count, 1)
def printer_webcam(ip): # This is very inefficient and should not be used in production. Use the nginx # redis based proxy pass instead # TODO maybe we can drop this in the dev env as well printer = printers.get_printer(ip) if printer is None: return abort(404) printer_inst = drivers.get_printer_instance(printer) webcam = printer_inst.webcam() if "stream" not in webcam: return abort(404) req = requests.get(webcam["stream"], stream=True) return Response( stream_with_context(req.iter_content()), content_type=req.headers["content-type"], )
def save_printer_data(**kwargs): has_record = printers.get_printer(kwargs["ip"]) if has_record is None and not kwargs["client_props"]["connected"]: return if has_record is None: printers.add_printer( **{ "name": None, "client_props": {"connected": False, "version": {}, "read_only": True}, **kwargs, } ) else: printers.update_printer( **{**has_record, **kwargs, **{"name": has_record["name"]}} )
def printer_modify_job(ip): printer = printers.get_printer(ip) if printer is None: return abort(404) data = request.json if not data: return abort(400) action = data.get("action", None) if not action: return abort(400) printer_inst = drivers.get_printer_instance(printer) try: if printer_inst.modify_current_job(action): return "", 204 return "", 409 except drivers.utils.PrinterDriverException as e: return abort(400, e)
def check_printer(printer_uuid): app.logger.debug("Checking printer %s" % printer_uuid) raw_printer = printers.get_printer(printer_uuid) raw_client = network_clients.get_network_client( raw_printer["network_client_uuid"]) printer_data = dict(raw_client) printer_data.update(raw_printer) printer = clients.get_printer_instance(printer_data) # websocket printers are not expected to change if printer.protocol in ["http", "https"]: if printer.hostname is not None: current_ip = network.get_avahi_address(printer.hostname) if current_ip is not None and current_ip != printer.ip: printer.ip = current_ip printer.update_network_base() hostname = network.get_avahi_hostname(printer.ip) if hostname is not None and hostname != printer.hostname: printer.hostname = hostname printer.update_network_base() now = datetime.now() if now.minute % 15 == 0 and printer.client_info.connected: printer.sniff() else: printer.is_alive() if printer.hostname != raw_client.get( "hostname") or printer.ip != raw_client.get("ip"): network_clients.update_network_client( uuid=raw_client["uuid"], hostname=printer.hostname, ip=printer.ip, ) printers.update_printer( uuid=printer.uuid, client_props={ "version": printer.client_info.version, "connected": printer.client_info.connected, "access_level": printer.client_info.access_level, "api_key": printer.client_info.api_key, "webcam": printer.client_info.webcam, "plugins": printer.client_info.plugins, }, )
def test_patch_printer_props(self): with app.test_client() as c: c.set_cookie("localhost", "access_token_cookie", TOKEN_ADMIN) response = c.patch( "/printers/%s" % self.uuid, headers={"x-csrf-token": TOKEN_ADMIN_CSRF}, json={ "name": "random-test-printer-name", "printer_props": { "filament_type": "PETG", "filament_color": "žluťoučká", "random": "key", }, }, ) self.assertEqual(response.status_code, 200) p = printers.get_printer(self.uuid) self.assertEqual(p["printer_props"]["filament_type"], "PETG") self.assertEqual(p["printer_props"]["filament_color"], "žluťoučká") self.assertTrue("random" not in p["printer_props"])
def test_list(self): with app.test_client() as c: c.set_cookie("localhost", "access_token_cookie", TOKEN_USER) response = c.get( "/organizations/%s/printjobs" % UUID_ORG, headers={"x-csrf-token": TOKEN_USER_CSRF}, ) self.assertEqual(response.status_code, 200) self.assertTrue("items" in response.json) if len(response.json["items"]) < 200: self.assertTrue("next" not in response.json) self.assertTrue(len(response.json["items"]) >= 2) self.assertTrue("uuid" in response.json["items"][0]) self.assertTrue("user_uuid" in response.json["items"][0]) self.assertTrue("username" in response.json["items"][0]) self.assertTrue("gcode_data" in response.json["items"][0]) self.assertTrue("printer_data" in response.json["items"][0]) self.assertTrue("started" in response.json["items"][0]) for item in response.json["items"]: printer = printers.get_printer(item["printer_uuid"]) self.assertEqual(printer["organization_uuid"], UUID_ORG)
def printjob_create(): data = request.json if not data: return abort(400) gcode_id = data.get("gcode", None) printer_ip = data.get("printer", None) if not gcode_id or not printer_ip: return abort(400) printer = printers.get_printer(printer_ip) if printer is None: return abort(404) gcode = gcodes.get_gcode(gcode_id) if gcode is None: return abort(404) try: printer_inst = drivers.get_printer_instance(printer) uploaded = printer_inst.upload_and_start_job(gcode["absolute_path"], gcode["path"]) if not uploaded: return abort(500, "Cannot upload the g-code to the printer") printjob_id = printjobs.add_printjob( gcode_id=gcode["id"], printer_ip=printer["ip"], gcode_data={ "id": gcode["id"], "filename": gcode["filename"], "size": gcode["size"], "available": True, }, printer_data={ "ip": printer["ip"], "name": printer["name"], "client": printer["client"], }, ) return jsonify({"id": printjob_id}), 201 except drivers.utils.PrinterDriverException: return abort(409)
def printer_patch(ip): printer = printers.get_printer(ip) if printer is None: return abort(404) data = request.json if not data: return abort(400) name = data.get("name", None) if not name: return abort(400) printer_inst = drivers.get_printer_instance(printer) printers.update_printer( name=name, hostname=printer_inst.hostname, ip=printer_inst.ip, client=printer_inst.client_name(), client_props={ "version": printer_inst.client.version, "connected": printer_inst.client.connected, "read_only": printer_inst.client.read_only, }, ) return "", 204
def printjob_create(org_uuid): data = request.json if not data: return abort(make_response(jsonify(message="Missing payload"), 400)) gcode_uuid = data.get("gcode", None) printer_uuid = data.get("printer", None) # FIXME: this should be part of the path if not gcode_uuid or not printer_uuid: return abort( make_response( jsonify(message="Missing gcode_uuid or printer_uuid"), 400)) printer = printers.get_printer(printer_uuid) if not printer or printer['organization_uuid'] != org_uuid: raise http_exceptions.UnprocessableEntity( f"Invalid printer {printer_uuid} - does not exist.") gcode = gcodes.get_gcode(gcode_uuid) if not gcode: raise http_exceptions.UnprocessableEntity( "Invalid gcode {gcode_uuid} - does not exist.") network_client = network_clients.get_network_client( printer["network_client_uuid"]) printer_data = dict(network_client) printer_data.update(dict(printer)) printer_inst = clients.get_printer_instance(printer_data) try: printer_inst.upload_and_start_job(gcode["absolute_path"], gcode["path"]) except DeviceInvalidState as e: raise http_exceptions.Conflict(*e.args) except DeviceCommunicationError as e: raise http_exceptions.GatewayTimeout(*e.args) # TODO: robin - add_printjob should be method of printer and printer a # method of organization printjob_uuid = printjobs.add_printjob( gcode_uuid=gcode["uuid"], organization_uuid=org_uuid, printer_uuid=printer["uuid"], user_uuid=get_current_user()["uuid"], gcode_data={ "uuid": gcode["uuid"], "filename": gcode["filename"], "size": gcode["size"], "available": True, }, # FIXME: printer data should be kept in printer object only printer_data={ "ip": printer_inst.ip, "port": printer_inst.port, "hostname": printer_inst.hostname, "name": printer_inst.name, "client": printer_inst.client, }, ) return ( jsonify({ "uuid": printjob_uuid, "user_uuid": get_current_user()["uuid"] }), 201, )
def printer_patch(uuid): try: uuidmodule.UUID(uuid, version=4) except ValueError: return abort(make_response("", 400)) printer = printers.get_printer(uuid) if printer is None: return abort(make_response("", 404)) data = request.json if not data: return abort(make_response("", 400)) name = data.get("name", printer["name"]) protocol = data.get("protocol", printer["protocol"]) api_key = data.get("api_key", printer["client_props"].get("api_key", None)) printer_props = data.get("printer_props", {}) # TODO it might be necessary to update ip, hostname, port as well eventually if not name or protocol not in ["http", "https"]: return abort(make_response("", 400)) printer_inst = clients.get_printer_instance(printer) printer_inst.add_api_key(api_key) if data.get("api_key", "-1") != "-1" and data.get( "api_key", "-1") != printer["client_props"].get("api_key", None): printer_inst.sniff( ) # this can probably be offloaded to check_printer task if printer_props: if not printer_inst.get_printer_props(): printer_inst.printer_props = {} # This is effectively the only place where printer_props "validation" happens printer_inst.get_printer_props().update({ k: printer_props[k] for k in [ "filament_type", "filament_color", "bed_type", "tool0_diameter", "note", ] if k in printer_props }) printers.update_printer( uuid=printer_inst.uuid, name=name, hostname=printer_inst.hostname, ip=printer_inst.ip, port=printer_inst.port, protocol=protocol, client=printer_inst.client_name(), client_props={ "version": printer_inst.client_info.version, "connected": printer_inst.client_info.connected, "access_level": printer_inst.client_info.access_level, "api_key": printer_inst.client_info.api_key, "webcam": printer_inst.webcam(), }, printer_props=printer_inst.get_printer_props(), ) # TODO cache webcam, job, status for a small amount of time in client return ( jsonify( make_printer_response(printer_inst, ["status", "webcam", "job"])), 200, )
def printjob_create(org_uuid): data = request.json if not data: return abort(make_response(jsonify(message="Missing payload"), 400)) gcode_uuid = data.get("gcode", None) printer_uuid = data.get("printer", None) if not gcode_uuid or not printer_uuid: return abort( make_response(jsonify(message="Missing gcode_uuid or printer_uuid"), 400) ) validate_uuid(gcode_uuid) validate_uuid(printer_uuid) printer = printers.get_printer(printer_uuid) if printer is None or printer["organization_uuid"] != org_uuid: return abort(make_response(jsonify(message="Not found"), 404)) gcode = gcodes.get_gcode(gcode_uuid) if gcode is None: return abort(make_response(jsonify(message="Not found"), 404)) try: network_client = network_clients.get_network_client( printer["network_client_uuid"] ) printer_data = dict(network_client) printer_data.update(dict(printer)) printer_inst = clients.get_printer_instance(printer_data) uploaded = printer_inst.upload_and_start_job( gcode["absolute_path"], gcode["path"] ) if not uploaded: return abort( make_response( jsonify(message="Cannot upload the g-code to the printer"), 500 ) ) printjob_uuid = guid.uuid4() printjobs.add_printjob( uuid=printjob_uuid, gcode_uuid=gcode["uuid"], organization_uuid=org_uuid, printer_uuid=printer["uuid"], user_uuid=get_current_user()["uuid"], gcode_data={ "uuid": gcode["uuid"], "filename": gcode["filename"], "size": gcode["size"], "available": True, }, printer_data={ "ip": printer_inst.ip, "port": printer_inst.port, "hostname": printer_inst.hostname, "name": printer_inst.name, "client": printer_inst.client, }, ) return ( jsonify({"uuid": printjob_uuid, "user_uuid": get_current_user()["uuid"]}), 201, ) except clients.utils.PrinterClientException as e: app.logger.error(e) return abort( make_response( jsonify(message="Cannot schedule a printjob: %s" % str(e)), 409 ) )