コード例 #1
0
ファイル: power.py プロジェクト: ihrapsa/moonraker
 def __init__(self, config: ConfigHelper) -> None:
     name_parts = config.get_name().split(maxsplit=1)
     if len(name_parts) != 2:
         raise config.error(f"Invalid Section Name: {config.get_name()}")
     self.server = config.get_server()
     self.name = name_parts[1]
     self.type: str = config.get('type')
     self.state: str = "init"
     self.locked_while_printing = config.getboolean('locked_while_printing',
                                                    False)
     self.off_when_shutdown = config.getboolean('off_when_shutdown', False)
     self.off_when_shutdown_delay = 0.
     if self.off_when_shutdown:
         self.off_when_shutdown_delay = config.getfloat(
             'off_when_shutdown_delay', 0., minval=0.)
     self.shutdown_timer_handle: Optional[asyncio.TimerHandle] = None
     self.restart_delay = 1.
     self.klipper_restart = config.getboolean(
         'restart_klipper_when_powered', False)
     if self.klipper_restart:
         self.restart_delay = config.getfloat('restart_delay', 1.)
         if self.restart_delay < .000001:
             raise config.error("Option 'restart_delay' must be above 0.0")
     self.bound_service: Optional[str] = config.get('bound_service', None)
     self.need_scheduled_restart = False
     self.on_when_queued = config.getboolean('on_when_upload_queued', False)
コード例 #2
0
ファイル: power.py プロジェクト: ihrapsa/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        super().__init__(config)
        self.mqtt: MQTTClient = self.server.load_component(config, 'mqtt')
        self.eventloop = self.server.get_event_loop()
        self.cmd_topic: str = config.get('command_topic')
        self.cmd_payload: JinjaTemplate = config.gettemplate('command_payload')
        self.retain_cmd_state = config.getboolean('retain_command_state',
                                                  False)
        self.query_topic: Optional[str] = config.get('query_topic', None)
        self.query_payload = config.gettemplate('query_payload', None)
        self.must_query = config.getboolean('query_after_command', False)
        if self.query_topic is not None:
            self.must_query = False

        self.state_topic: str = config.get('state_topic')
        self.state_timeout = config.getfloat('state_timeout', 2.)
        self.state_response = config.load_template('state_response_template',
                                                   "{payload}")
        self.qos: Optional[int] = config.getint('qos',
                                                None,
                                                minval=0,
                                                maxval=2)
        self.mqtt.subscribe_topic(self.state_topic, self._on_state_update,
                                  self.qos)
        self.query_response: Optional[asyncio.Future] = None
        self.request_mutex = asyncio.Lock()
        self.server.register_event_handler("mqtt:connected",
                                           self._on_mqtt_connected)
        self.server.register_event_handler("mqtt:disconnected",
                                           self._on_mqtt_disconnected)
コード例 #3
0
ファイル: test_config.py プロジェクト: Arksine/moonraker
 def test_get_int_deprecate(self, test_config: ConfigHelper):
     server = test_config.get_server()
     test_config.getboolean("test_bool", deprecate=True)
     expected = (
         f"[test_options]: Option 'test_bool' is "
         "deprecated, see the configuration documention "
         "at https://moonraker.readthedocs.io/en/latest/configuration")
     assert expected in server.warnings
コード例 #4
0
ファイル: webcam.py プロジェクト: Arksine/moonraker
 def from_config(cls, config: ConfigHelper) -> WebCam:
     webcam: Dict[str, Any] = {}
     webcam["name"] = config.get_name().split(maxsplit=1)[-1]
     webcam["location"] = config.get("location", "printer")
     webcam["service"] = config.get("service", "mjpegstreamer")
     webcam["target_fps"] = config.getint("target_fps", 15)
     webcam["stream_url"] = config.get("stream_url")
     webcam["snapshot_url"] = config.get("snapshot_url")
     webcam["flip_horizontal"] = config.getboolean("flip_horizontal", False)
     webcam["flip_vertical"] = config.getboolean("flip_vertical", False)
     webcam["rotation"] = config.getint("rotation", 0)
     if webcam["rotation"] not in [0, 90, 180, 270]:
         raise config.error("Invalid value for option 'rotation'")
     webcam["source"] = "config"
     return cls(config.get_server(), **webcam)
コード例 #5
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.debug_enabled = config.getboolean('enable_repo_debug', False)
        if self.debug_enabled:
            logging.warning("UPDATE MANAGER: REPO DEBUG ENABLED")
        shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
        self.scmd_error = shell_cmd.error
        self.build_shell_command = shell_cmd.build_shell_command
        self.pkg_updater: Optional[PackageDeploy] = None
        self.http_client = AsyncHTTPClient()
        self.github_request_cache: Dict[str, CachedGithubResponse] = {}

        # database management
        db: DBComp = self.server.lookup_component('database')
        db.register_local_namespace("update_manager")
        self.umdb = db.wrap_namespace("update_manager")

        # Refresh Time Tracking (default is to refresh every 28 days)
        reresh_interval = config.getint('refresh_interval', 672)
        # Convert to seconds
        self.refresh_interval = reresh_interval * 60 * 60

        # GitHub API Rate Limit Tracking
        self.gh_rate_limit: Optional[int] = None
        self.gh_limit_remaining: Optional[int] = None
        self.gh_limit_reset_time: Optional[float] = None

        # Update In Progress Tracking
        self.cur_update_app: Optional[str] = None
        self.cur_update_id: Optional[int] = None
        self.full_complete: bool = False
コード例 #6
0
ファイル: database.py プロジェクト: vladimir-poleh/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.namespaces: Dict[str, object] = {}
        self.enable_debug = config.getboolean("enable_database_debug", False)
        self.database_path = os.path.expanduser(
            config.get('database_path', "~/.moonraker_database"))
        if not os.path.isdir(self.database_path):
            os.mkdir(self.database_path)
        self.lmdb_env = lmdb.open(self.database_path,
                                  map_size=MAX_DB_SIZE,
                                  max_dbs=MAX_NAMESPACES)
        with self.lmdb_env.begin(write=True, buffers=True) as txn:
            # lookup existing namespaces
            cursor = txn.cursor()
            remaining = cursor.first()
            while remaining:
                key = bytes(cursor.key())
                self.namespaces[key.decode()] = self.lmdb_env.open_db(key, txn)
                remaining = cursor.next()
            cursor.close()
            if "moonraker" not in self.namespaces:
                mrdb = self.lmdb_env.open_db(b"moonraker", txn)
                self.namespaces["moonraker"] = mrdb
                txn.put(b'database_version',
                        self._encode_value(DATABASE_VERSION),
                        db=mrdb)
        # Read out all namespaces to remove any invalid keys on init
        for ns in self.namespaces.keys():
            self._get_namespace(ns)
        # Protected Namespaces have read-only API access.  Write access can
        # be granted by enabling the debug option.  Forbidden namespaces
        # have no API access.  This cannot be overridden.
        self.protected_namespaces = set(
            self.get_item("moonraker", "database.protected_namespaces",
                          ["moonraker"]))
        self.forbidden_namespaces = set(
            self.get_item("moonraker", "database.forbidden_namespaces", []))
        debug_counter: int = self.get_item("moonraker",
                                           "database.debug_counter", 0)
        if self.enable_debug:
            debug_counter += 1
            self.insert_item("moonraker", "database.debug_counter",
                             debug_counter)
        if debug_counter:
            logging.info(f"Database Debug Count: {debug_counter}")

        # Track unsafe shutdowns
        unsafe_shutdowns: int = self.get_item("moonraker",
                                              "database.unsafe_shutdowns", 0)
        logging.info(f"Unsafe Shutdown Count: {unsafe_shutdowns}")
        # Increment unsafe shutdown counter.  This will be reset if
        # moonraker is safely restarted
        self.insert_item("moonraker", "database.unsafe_shutdowns",
                         unsafe_shutdowns + 1)
        self.server.register_endpoint("/server/database/list", ['GET'],
                                      self._handle_list_request)
        self.server.register_endpoint("/server/database/item",
                                      ["GET", "POST", "DELETE"],
                                      self._handle_item_request)
コード例 #7
0
ファイル: power.py プロジェクト: Swift-Tester/moonraker
 def __init__(self, config: ConfigHelper) -> None:
     name_parts = config.get_name().split(maxsplit=1)
     if len(name_parts) != 2:
         raise config.error(f"Invalid Section Name: {config.get_name()}")
     self.server = config.get_server()
     self.name = name_parts[1]
     self.type: str = config.get('type')
     self.state: str = "init"
     self.locked_while_printing = config.getboolean('locked_while_printing',
                                                    False)
     self.off_when_shutdown = config.getboolean('off_when_shutdown', False)
     self.restart_delay = 1.
     self.klipper_restart = config.getboolean(
         'restart_klipper_when_powered', False)
     if self.klipper_restart:
         self.restart_delay = config.getfloat('restart_delay', 1.)
         if self.restart_delay < .000001:
             raise config.error("Option 'restart_delay' must be above 0.0")
コード例 #8
0
ファイル: announcements.py プロジェクト: Arksine/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.entry_mgr = EntryManager(config)
        self.eventloop = self.server.get_event_loop()
        self.update_timer = self.eventloop.register_timer(
            self._handle_update_timer
        )
        self.request_lock = asyncio.Lock()
        self.dev_mode = config.getboolean("dev_mode", False)
        self.subscriptions: Dict[str, RssFeed] = {
            "moonraker": RssFeed("moonraker", self.entry_mgr, self.dev_mode),
            "klipper": RssFeed("klipper", self.entry_mgr, self.dev_mode)
        }
        self.stored_feeds: List[str] = []
        sub_list: List[str] = config.getlist("subscriptions", [])
        self.configured_feeds: List[str] = ["moonraker", "klipper"]
        for sub in sub_list:
            sub = sub.lower()
            if sub in self.subscriptions:
                continue
            self.configured_feeds.append(sub)
            self.subscriptions[sub] = RssFeed(
                sub, self.entry_mgr, self.dev_mode
            )

        self.server.register_endpoint(
            "/server/announcements/list", ["GET"],
            self._list_announcements
        )
        self.server.register_endpoint(
            "/server/announcements/dismiss", ["POST"],
            self._handle_dismiss_request
        )
        self.server.register_endpoint(
            "/server/announcements/update", ["POST"],
            self._handle_update_request
        )
        self.server.register_endpoint(
            "/server/announcements/feed", ["POST", "DELETE"],
            self._handle_feed_request
        )
        self.server.register_endpoint(
            "/server/announcements/feeds", ["GET"],
            self._handle_list_feeds
        )
        self.server.register_notification(
            "announcements:dismissed", "announcement_dismissed"
        )
        self.server.register_notification(
            "announcements:entries_updated", "announcement_update"
        )
        self.server.register_notification(
            "announcements:dismiss_wake", "announcement_wake"
        )
コード例 #9
0
ファイル: app.py プロジェクト: vladimir-poleh/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.http_server: Optional[HTTPServer] = None
        self.secure_server: Optional[HTTPServer] = None
        self.api_cache: Dict[str, APIDefinition] = {}
        self.registered_base_handlers: List[str] = []
        self.max_upload_size = config.getint('max_upload_size', 1024)
        self.max_upload_size *= 1024 * 1024

        # SSL config
        self.cert_path: str = self._get_path_option(
            config, 'ssl_certificate_path')
        self.key_path: str = self._get_path_option(
            config, 'ssl_key_path')

        # Set Up Websocket and Authorization Managers
        self.wsm = WebsocketManager(self.server)
        self.api_transports: Dict[str, APITransport] = {
            "websocket": self.wsm
        }

        mimetypes.add_type('text/plain', '.log')
        mimetypes.add_type('text/plain', '.gcode')
        mimetypes.add_type('text/plain', '.cfg')

        self.debug = config.getboolean('enable_debug_logging', False)
        log_level = logging.DEBUG if self.debug else logging.INFO
        logging.getLogger().setLevel(log_level)
        app_args: Dict[str, Any] = {
            'serve_traceback': self.debug,
            'websocket_ping_interval': 10,
            'websocket_ping_timeout': 30,
            'parent': self,
            'default_handler_class': AuthorizedErrorHandler,
            'default_handler_args': {},
            'log_function': self.log_request
        }

        # Set up HTTP only requests
        self.mutable_router = MutableRouter(self)
        app_handlers: List[Any] = [
            (AnyMatches(), self.mutable_router),
            (r"/websocket", WebSocket),
            (r"/server/redirect", RedirectHandler)]
        self.app = tornado.web.Application(app_handlers, **app_args)
        self.get_handler_delegate = self.app.get_handler_delegate

        # Register handlers
        logfile = self.server.get_app_args().get('log_file')
        if logfile:
            self.register_static_file_handler(
                "moonraker.log", logfile, force=True)
        self.register_static_file_handler(
            "klippy.log", DEFAULT_KLIPPY_LOG_PATH, force=True)
コード例 #10
0
ファイル: job_queue.py プロジェクト: th33xitus/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.queued_jobs: Dict[str, QueuedJob] = {}
        self.lock = asyncio.Lock()
        self.load_on_start = config.getboolean("load_on_startup", False)
        self.automatic = config.getboolean("automatic_transition", False)
        self.queue_state: str = "ready" if self.automatic else "paused"
        self.job_delay = config.getfloat("job_transition_delay", 0.01)
        if self.job_delay <= 0.:
            raise config.error(
                "Value for option 'job_transition_delay' in section [job_queue]"
                " must be above 0.0")
        self.job_transition_gcode = config.get(
            "job_transition_gcode", "").strip()
        self.pop_queue_handle: Optional[asyncio.TimerHandle] = None

        self.server.register_event_handler(
            "server:klippy_ready", self._handle_ready)
        self.server.register_event_handler(
            "server:klippy_shutdown", self._handle_shutdown)
        self.server.register_event_handler(
            "job_state:complete", self._on_job_complete)
        self.server.register_event_handler(
            "job_state:error", self._on_job_abort)
        self.server.register_event_handler(
            "job_state:cancelled", self._on_job_abort)

        self.server.register_notification("job_queue:job_queue_changed")
        self.server.register_remote_method("pause_job_queue", self.pause_queue)
        self.server.register_remote_method("start_job_queue",
                                           self.start_queue)

        self.server.register_endpoint(
            "/server/job_queue/job", ['POST', 'DELETE'],
            self._handle_job_request)
        self.server.register_endpoint(
            "/server/job_queue/pause", ['POST'], self._handle_pause_queue)
        self.server.register_endpoint(
            "/server/job_queue/start", ['POST'], self._handle_start_queue)
        self.server.register_endpoint(
            "/server/job_queue/status", ['GET'], self._handle_queue_status)
コード例 #11
0
    def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper,
                 app_params: Optional[Dict[str, Any]]) -> None:
        super().__init__(config, cmd_helper)
        self.config = config
        self.app_params = app_params
        self.debug = self.cmd_helper.is_debug_enabled()
        if app_params is not None:
            self.channel: str = app_params['channel']
            self.path: pathlib.Path = pathlib.Path(
                app_params['path']).expanduser().resolve()
            executable: Optional[str] = app_params['executable']
            self.type = CHANNEL_TO_TYPE[self.channel]
        else:
            self.type = config.get('type')
            self.channel = TYPE_TO_CHANNEL[self.type]
            self.path = pathlib.Path(config.get('path')).expanduser().resolve()
            executable = config.get('env', None)
        if self.channel not in CHANNEL_TO_TYPE.keys():
            raise config.error(f"Invalid Channel '{self.channel}' for config "
                               f"section [{config.get_name()}]")
        self._verify_path(config, 'path', self.path)
        self.executable: Optional[pathlib.Path] = None
        self.venv_args: Optional[str] = None
        if executable is not None:
            self.executable = pathlib.Path(executable).expanduser().resolve()
            self._verify_path(config, 'env', self.executable)
            self.venv_args = config.get('venv_args', None)

        self.is_service = config.getboolean("is_system_service", True)
        self.need_channel_update = False
        self._is_valid = False

        # We need to fetch all potential options for an Application.  Not
        # all options apply to each subtype, however we can't limit the
        # options in children if we want to switch between channels and
        # satisfy the confighelper's requirements.
        self.origin: str = config.get('origin')
        self.primary_branch = config.get("primary_branch", "master")
        self.npm_pkg_json: Optional[pathlib.Path] = None
        if config.get("enable_node_updates", False):
            self.npm_pkg_json = self.path.joinpath("package-lock.json")
            self._verify_path(config, 'enable_node_updates', self.npm_pkg_json)
        self.python_reqs: Optional[pathlib.Path] = None
        if self.executable is not None:
            self.python_reqs = self.path.joinpath(config.get("requirements"))
            self._verify_path(config, 'requirements', self.python_reqs)
        self.install_script: Optional[pathlib.Path] = None
        install_script = config.get('install_script', None)
        if install_script is not None:
            self.install_script = self.path.joinpath(install_script).resolve()
            self._verify_path(config, 'install_script', self.install_script)
コード例 #12
0
ファイル: ldap.py プロジェクト: Arksine/moonraker
 def __init__(self, config: ConfigHelper) -> None:
     self.server = config.get_server()
     self.ldap_host = config.get('ldap_host')
     self.ldap_port = config.getint("ldap_port", None)
     self.ldap_secure = config.getboolean("ldap_secure", False)
     base_dn_template = config.gettemplate('base_dn')
     self.base_dn = base_dn_template.render()
     self.group_dn: Optional[str] = None
     group_dn_template = config.gettemplate("group_dn", None)
     if group_dn_template is not None:
         self.group_dn = group_dn_template.render()
     self.active_directory = config.getboolean('is_active_directory', False)
     self.bind_dn: Optional[str] = None
     self.bind_password: Optional[str] = None
     bind_dn_template = config.gettemplate('bind_dn', None)
     bind_pass_template = config.gettemplate('bind_password', None)
     if bind_dn_template is not None:
         self.bind_dn = bind_dn_template.render()
         if bind_pass_template is None:
             raise config.error("Section [ldap]: Option 'bind_password' is "
                                "required when 'bind_dn' is provided")
         self.bind_password = bind_pass_template.render()
     self.lock = asyncio.Lock()
コード例 #13
0
ファイル: power.py プロジェクト: ihrapsa/moonraker
 def __init__(self,
              config: ConfigHelper,
              initial_val: Optional[int] = None) -> None:
     super().__init__(config)
     self.initial_state = config.getboolean('initial_state', False)
     self.timer: Optional[float] = config.getfloat('timer', None)
     if self.timer is not None and self.timer < 0.000001:
         raise config.error(
             f"Option 'timer' in section [{config.get_name()}] must "
             "be above 0.0")
     self.timer_handle: Optional[asyncio.TimerHandle] = None
     if initial_val is None:
         initial_val = int(self.initial_state)
     self.gpio_out = config.getgpioout('pin', initial_value=initial_val)
コード例 #14
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.debug_enabled = config.getboolean('enable_repo_debug', False)
        if self.debug_enabled:
            logging.warning("UPDATE MANAGER: REPO DEBUG ENABLED")
        shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
        self.scmd_error = shell_cmd.error
        self.build_shell_command = shell_cmd.build_shell_command
        self.pkg_updater: Optional[PackageDeploy] = None

        AsyncHTTPClient.configure(None, defaults=dict(user_agent="Moonraker"))
        self.http_client = AsyncHTTPClient()
        self.github_request_cache: Dict[str, CachedGithubResponse] = {}

        # GitHub API Rate Limit Tracking
        self.gh_rate_limit: Optional[int] = None
        self.gh_limit_remaining: Optional[int] = None
        self.gh_limit_reset_time: Optional[float] = None

        # Update In Progress Tracking
        self.cur_update_app: Optional[str] = None
        self.cur_update_id: Optional[int] = None
        self.full_complete: bool = False
コード例 #15
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.event_loop = self.server.get_event_loop()
        self.address: str = config.get('address')
        self.port: int = config.getint('port', 1883)
        user = config.gettemplate('username', None)
        self.user_name: Optional[str] = None
        if user:
            self.user_name = user.render()
        pw_file_path = config.get('password_file', None, deprecate=True)
        pw_template = config.gettemplate('password', None)
        self.password: Optional[str] = None
        if pw_file_path is not None:
            pw_file = pathlib.Path(pw_file_path).expanduser().absolute()
            if not pw_file.exists():
                raise config.error(
                    f"Password file '{pw_file}' does not exist")
            self.password = pw_file.read_text().strip()
        if pw_template is not None:
            self.password = pw_template.render()
        protocol = config.get('mqtt_protocol', "v3.1.1")
        self.protocol = MQTT_PROTOCOLS.get(protocol, None)
        if self.protocol is None:
            raise config.error(
                f"Invalid value '{protocol}' for option 'mqtt_protocol' "
                "in section [mqtt]. Must be one of "
                f"{MQTT_PROTOCOLS.values()}")
        self.instance_name = config.get('instance_name', socket.gethostname())
        if '+' in self.instance_name or '#' in self.instance_name:
            raise config.error(
                "Option 'instance_name' in section [mqtt] cannot "
                "contain a wildcard.")
        self.qos = config.getint("default_qos", 0)
        if self.qos > 2 or self.qos < 0:
            raise config.error(
                "Option 'default_qos' in section [mqtt] must be "
                "between 0 and 2")
        self.client = paho_mqtt.Client(protocol=self.protocol)
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
        self.client.on_publish = self._on_publish
        self.client.on_subscribe = self._on_subscribe
        self.client.on_unsubscribe = self._on_unsubscribe
        self.connect_evt: asyncio.Event = asyncio.Event()
        self.disconnect_evt: Optional[asyncio.Event] = None
        self.reconnect_task: Optional[asyncio.Task] = None
        self.subscribed_topics: SubscribedDict = {}
        self.pending_responses: List[asyncio.Future] = []
        self.pending_acks: Dict[int, asyncio.Future] = {}

        self.server.register_endpoint(
            "/server/mqtt/publish", ["POST"],
            self._handle_publish_request,
            transports=["http", "websocket", "internal"])
        self.server.register_endpoint(
            "/server/mqtt/subscribe", ["POST"],
            self._handle_subscription_request,
            transports=["http", "websocket", "internal"])

        # Subscribe to API requests
        self.json_rpc = JsonRPC(transport="MQTT")
        self.api_request_topic = f"{self.instance_name}/moonraker/api/request"
        self.api_resp_topic = f"{self.instance_name}/moonraker/api/response"
        self.klipper_status_topic = f"{self.instance_name}/klipper/status"
        self.moonraker_status_topic = f"{self.instance_name}/moonraker/status"
        status_cfg: Dict[str, Any] = config.getdict("status_objects", {},
                                                    allow_empty_fields=True)
        self.status_objs: Dict[str, Any] = {}
        for key, val in status_cfg.items():
            if val is not None:
                self.status_objs[key] = [v.strip() for v in val.split(',')
                                         if v.strip()]
            else:
                self.status_objs[key] = None
        if status_cfg:
            logging.debug(f"MQTT: Status Objects Set: {self.status_objs}")
            self.server.register_event_handler("server:klippy_identified",
                                               self._handle_klippy_identified)

        self.timestamp_deque: Deque = deque(maxlen=20)
        self.api_qos = config.getint('api_qos', self.qos)
        if config.getboolean("enable_moonraker_api", True):
            api_cache = self.server.register_api_transport("mqtt", self)
            for api_def in api_cache.values():
                if "mqtt" in api_def.supported_transports:
                    self.register_api_handler(api_def)
            self.subscribe_topic(self.api_request_topic,
                                 self._process_api_request,
                                 self.api_qos)

        self.server.register_remote_method("publish_mqtt_topic",
                                           self._publish_from_klipper)
        logging.info(
            f"\nReserved MQTT topics:\n"
            f"API Request: {self.api_request_topic}\n"
            f"API Response: {self.api_resp_topic}\n"
            f"Moonraker Status: {self.moonraker_status_topic}\n"
            f"Klipper Status: {self.klipper_status_topic}")
コード例 #16
0
ファイル: paneldue.py プロジェクト: th33xitus/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.event_loop = self.server.get_event_loop()
        self.file_manager: FMComp = \
            self.server.lookup_component('file_manager')
        self.klippy_apis: APIComp = \
            self.server.lookup_component('klippy_apis')
        self.kinematics: str = "none"
        self.machine_name = config.get('machine_name', "Klipper")
        self.firmware_name: str = "Repetier | Klipper"
        self.last_message: Optional[str] = None
        self.last_gcode_response: Optional[str] = None
        self.current_file: str = ""
        self.file_metadata: Dict[str, Any] = {}
        self.enable_checksum = config.getboolean('enable_checksum', True)
        self.debug_queue: Deque[str] = deque(maxlen=100)

        # Initialize tracked state.
        self.printer_state: Dict[str, Dict[str, Any]] = {
            'gcode_move': {},
            'toolhead': {},
            'virtual_sdcard': {},
            'fan': {},
            'display_status': {},
            'print_stats': {},
            'idle_timeout': {},
            'gcode_macro PANELDUE_BEEP': {}
        }
        self.extruder_count: int = 0
        self.heaters: List[str] = []
        self.is_ready: bool = False
        self.is_shutdown: bool = False
        self.initialized: bool = False
        self.cq_busy: bool = False
        self.gq_busy: bool = False
        self.command_queue: List[Tuple[FlexCallback, Any, Any]] = []
        self.gc_queue: List[str] = []
        self.last_printer_state: str = 'O'
        self.last_update_time: float = 0.

        # Set up macros
        self.confirmed_gcode: str = ""
        self.mbox_sequence: int = 0
        self.available_macros: Dict[str, str] = {}
        self.confirmed_macros = {
            "RESTART": "RESTART",
            "FIRMWARE_RESTART": "FIRMWARE_RESTART"
        }
        macros = config.getlist('macros', None)
        if macros is not None:
            # The macro's configuration name is the key, whereas the full
            # command is the value
            self.available_macros = {m.split()[0]: m for m in macros}
        conf_macros = config.getlist('confirmed_macros', None)
        if conf_macros is not None:
            # The macro's configuration name is the key, whereas the full
            # command is the value
            self.confirmed_macros = {m.split()[0]: m for m in conf_macros}
        self.available_macros.update(self.confirmed_macros)

        self.non_trivial_keys = config.getlist('non_trivial_keys',
                                               ["Klipper state"])
        self.ser_conn = SerialConnection(config, self)
        logging.info("PanelDue Configured")

        # Register server events
        self.server.register_event_handler("server:klippy_ready",
                                           self._process_klippy_ready)
        self.server.register_event_handler("server:klippy_shutdown",
                                           self._process_klippy_shutdown)
        self.server.register_event_handler("server:klippy_disconnect",
                                           self._process_klippy_disconnect)
        self.server.register_event_handler("server:status_update",
                                           self.handle_status_update)
        self.server.register_event_handler("server:gcode_response",
                                           self.handle_gcode_response)

        self.server.register_remote_method("paneldue_beep", self.paneldue_beep)

        # These commands are directly executued on the server and do not to
        # make a request to Klippy
        self.direct_gcodes: Dict[str, FlexCallback] = {
            'M20': self._run_paneldue_M20,
            'M30': self._run_paneldue_M30,
            'M36': self._run_paneldue_M36,
            'M408': self._run_paneldue_M408
        }

        # These gcodes require special parsing or handling prior to being
        # sent via Klippy's "gcode/script" api command.
        self.special_gcodes: Dict[str, Callable[[List[str]], str]] = {
            'M0': lambda args: "CANCEL_PRINT",
            'M23': self._prepare_M23,
            'M24': lambda args: "RESUME",
            'M25': lambda args: "PAUSE",
            'M32': self._prepare_M32,
            'M98': self._prepare_M98,
            'M120': lambda args: "SAVE_GCODE_STATE STATE=PANELDUE",
            'M121': lambda args: "RESTORE_GCODE_STATE STATE=PANELDUE",
            'M290': self._prepare_M290,
            'M292': self._prepare_M292,
            'M999': lambda args: "FIRMWARE_RESTART"
        }
コード例 #17
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.address: str = config.get('address')
        self.port: int = config.getint('port', 1883)
        self.user_name = config.get('username', None)
        pw_file_path = config.get('password_file', None)
        self.password: Optional[str] = None
        if pw_file_path is not None:
            pw_file = pathlib.Path(pw_file_path).expanduser().absolute()
            if not pw_file.exists():
                raise config.error(f"Password file '{pw_file}' does not exist")
            self.password = pw_file.read_text().strip()
        protocol = config.get('mqtt_protocol', "v3.1.1")
        self.protocol = MQTT_PROTOCOLS.get(protocol, None)
        if self.protocol is None:
            raise config.error(
                f"Invalid value '{protocol}' for option 'mqtt_protocol' "
                "in section [mqtt]. Must be one of "
                f"{MQTT_PROTOCOLS.values()}")
        self.instance_name = config.get('instance_name', socket.gethostname())
        if '+' in self.instance_name or '#' in self.instance_name:
            raise config.error(
                "Option 'instance_name' in section [mqtt] cannot "
                "contain a wildcard.")
        self.qos = config.getint("default_qos", 0)
        if self.qos > 2 or self.qos < 0:
            raise config.error(
                "Option 'default_qos' in section [mqtt] must be "
                "between 0 and 2")
        self.client = paho_mqtt.Client(protocol=self.protocol)
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
        self.client.on_publish = self._on_publish
        self.client.on_subscribe = self._on_subscribe
        self.client.on_unsubscribe = self._on_unsubscribe
        self.connect_evt: asyncio.Event = asyncio.Event()
        self.disconnect_evt: Optional[asyncio.Event] = None
        self.reconnect_task: Optional[asyncio.Task] = None
        self.subscribed_topics: SubscribedDict = {}
        self.pending_responses: List[asyncio.Future] = []
        self.pending_acks: Dict[int, asyncio.Future] = {}

        self.server.register_endpoint("/server/mqtt/publish", ["POST"],
                                      self._handle_publish_request,
                                      transports=["http", "websocket"])
        self.server.register_endpoint("/server/mqtt/subscribe", ["POST"],
                                      self._handle_subscription_request,
                                      transports=["http", "websocket"])

        # Subscribe to API requests
        self.json_rpc = JsonRPC(transport="MQTT")
        self.api_request_topic = f"{self.instance_name}/moonraker/api/request"
        self.api_resp_topic = f"{self.instance_name}/moonraker/api/response"
        self.timestamp_deque: Deque = deque(maxlen=20)
        self.api_qos = config.getint('api_qos', self.qos)
        if config.getboolean("enable_moonraker_api", True):
            api_cache = self.server.register_api_transport("mqtt", self)
            for api_def in api_cache.values():
                if "mqtt" in api_def.supported_transports:
                    self.register_api_handler(api_def)
            self.subscribe_topic(self.api_request_topic,
                                 self._process_api_request, self.api_qos)
            logging.info(
                f"Moonraker API topics - Request: {self.api_request_topic}, "
                f"Response: {self.api_resp_topic}")

        IOLoop.current().spawn_callback(self._initialize)
コード例 #18
0
ファイル: database.py プロジェクト: Arksine/moonraker
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.eventloop = self.server.get_event_loop()
        self.namespaces: Dict[str, object] = {}
        self.thread_lock = ThreadLock()
        self.database_path = os.path.expanduser(
            config.get('database_path', "~/.moonraker_database"))
        if not os.path.isdir(self.database_path):
            os.mkdir(self.database_path)
        self.lmdb_env = lmdb.open(self.database_path,
                                  map_size=MAX_DB_SIZE,
                                  max_dbs=MAX_NAMESPACES)
        with self.lmdb_env.begin(write=True, buffers=True) as txn:
            # lookup existing namespaces
            with txn.cursor() as cursor:
                remaining = cursor.first()
                while remaining:
                    key = bytes(cursor.key())
                    self.namespaces[key.decode()] = self.lmdb_env.open_db(
                        key, txn)
                    remaining = cursor.next()
            if "moonraker" not in self.namespaces:
                mrdb = self.lmdb_env.open_db(b"moonraker", txn)
                self.namespaces["moonraker"] = mrdb
                txn.put(b'database_version',
                        self._encode_value(DATABASE_VERSION),
                        db=mrdb)
            # Iterate through all records, checking for invalid keys
            for ns, db in self.namespaces.items():
                with txn.cursor(db=db) as cursor:
                    remaining = cursor.first()
                    while remaining:
                        key_buf = cursor.key()
                        try:
                            decoded_key = bytes(key_buf).decode()
                        except Exception:
                            logging.info("Database Key Decode Error")
                            decoded_key = ''
                        if not decoded_key:
                            hex_key = bytes(key_buf).hex()
                            try:
                                invalid_val = self._decode_value(
                                    cursor.value())
                            except Exception:
                                invalid_val = ""
                            logging.info(
                                f"Invalid Key '{hex_key}' found in namespace "
                                f"'{ns}', dropping value: {repr(invalid_val)}")
                            try:
                                remaining = cursor.delete()
                            except Exception:
                                logging.exception("Error Deleting LMDB Key")
                            else:
                                continue
                        remaining = cursor.next()

        # Protected Namespaces have read-only API access.  Write access can
        # be granted by enabling the debug option.  Forbidden namespaces
        # have no API access.  This cannot be overridden.
        self.protected_namespaces = set(
            self.get_item("moonraker", "database.protected_namespaces",
                          ["moonraker"]).result())
        self.forbidden_namespaces = set(
            self.get_item("moonraker", "database.forbidden_namespaces",
                          []).result())
        # Remove stale debug counter
        config.getboolean("enable_database_debug", False, deprecate=True)
        try:
            self.delete_item("moonraker", "database.debug_counter")
        except Exception:
            pass
        # Track unsafe shutdowns
        unsafe_shutdowns: int = self.get_item("moonraker",
                                              "database.unsafe_shutdowns",
                                              0).result()
        msg = f"Unsafe Shutdown Count: {unsafe_shutdowns}"
        self.server.add_log_rollover_item("database", msg)

        # Increment unsafe shutdown counter.  This will be reset if
        # moonraker is safely restarted
        self.insert_item("moonraker", "database.unsafe_shutdowns",
                         unsafe_shutdowns + 1)
        self.server.register_endpoint("/server/database/list", ['GET'],
                                      self._handle_list_request)
        self.server.register_endpoint("/server/database/item",
                                      ["GET", "POST", "DELETE"],
                                      self._handle_item_request)
コード例 #19
0
ファイル: app_deploy.py プロジェクト: Arksine/moonraker
    def __init__(self, config: ConfigHelper,
                 cmd_helper: CommandHelper) -> None:
        super().__init__(config, cmd_helper, prefix="Application")
        self.config = config
        self.debug = self.cmd_helper.is_debug_enabled()
        type_choices = list(TYPE_TO_CHANNEL.keys())
        self.type = config.get('type').lower()
        if self.type not in type_choices:
            raise config.error(
                f"Config Error: Section [{config.get_name()}], Option "
                f"'type: {self.type}': value must be one "
                f"of the following choices: {type_choices}")
        self.channel = config.get("channel", TYPE_TO_CHANNEL[self.type])
        if self.type == "zip_beta":
            self.server.add_warning(
                f"Config Section [{config.get_name()}], Option 'type: "
                "zip_beta', value 'zip_beta' is deprecated.  Set 'type' "
                "to zip and 'channel' to 'beta'")
            self.type = "zip"
        self.path = pathlib.Path(config.get('path')).expanduser().resolve()
        executable = config.get('env', None)
        if self.channel not in SUPPORTED_CHANNELS[self.type]:
            raise config.error(
                f"Invalid Channel '{self.channel}' for config "
                f"section [{config.get_name()}], type: {self.type}")
        self._verify_path(config, 'path', self.path)
        self.executable: Optional[pathlib.Path] = None
        self.pip_exe: Optional[pathlib.Path] = None
        self.venv_args: Optional[str] = None
        if executable is not None:
            self.executable = pathlib.Path(executable).expanduser()
            self.pip_exe = self.executable.parent.joinpath("pip")
            if not self.pip_exe.exists():
                self.server.add_warning(
                    f"Update Manger {self.name}: Unable to locate pip "
                    "executable")
            self._verify_path(config, 'env', self.executable)
            self.venv_args = config.get('venv_args', None)

        self.info_tags: List[str] = config.getlist("info_tags", [])
        self.managed_services: List[str] = []
        svc_default = []
        if config.getboolean("is_system_service", True):
            svc_default.append(self.name)
        svc_choices = [self.name, "klipper", "moonraker"]
        services: List[str] = config.getlist("managed_services",
                                             svc_default,
                                             separator=None)
        for svc in services:
            if svc not in svc_choices:
                raw = " ".join(services)
                self.server.add_warning(
                    f"[{config.get_name()}]: Option 'restart_action: {raw}' "
                    f"contains an invalid value '{svc}'.  All values must be "
                    f"one of the following choices: {svc_choices}")
                break
        for svc in svc_choices:
            if svc in services and svc not in self.managed_services:
                self.managed_services.append(svc)
        logging.debug(
            f"Extension {self.name} managed services: {self.managed_services}")
        # We need to fetch all potential options for an Application.  Not
        # all options apply to each subtype, however we can't limit the
        # options in children if we want to switch between channels and
        # satisfy the confighelper's requirements.
        self.moved_origin: Optional[str] = config.get('moved_origin', None)
        self.origin: str = config.get('origin')
        self.primary_branch = config.get("primary_branch", "master")
        self.npm_pkg_json: Optional[pathlib.Path] = None
        if config.getboolean("enable_node_updates", False):
            self.npm_pkg_json = self.path.joinpath("package-lock.json")
            self._verify_path(config, 'enable_node_updates', self.npm_pkg_json)
        self.python_reqs: Optional[pathlib.Path] = None
        if self.executable is not None:
            self.python_reqs = self.path.joinpath(config.get("requirements"))
            self._verify_path(config, 'requirements', self.python_reqs)
        self.install_script: Optional[pathlib.Path] = None
        install_script = config.get('install_script', None)
        if install_script is not None:
            self.install_script = self.path.joinpath(install_script).resolve()
            self._verify_path(config, 'install_script', self.install_script)
コード例 #20
0
ファイル: test_config.py プロジェクト: Arksine/moonraker
 def test_get_float_default(self, test_config: ConfigHelper):
     assert test_config.getboolean("invalid_option", None) is None
コード例 #21
0
ファイル: test_config.py プロジェクト: Arksine/moonraker
 def test_get_float_fail(self, test_config: ConfigHelper):
     with pytest.raises(ConfigError):
         test_config.getboolean("invalid_option")
コード例 #22
0
ファイル: test_config.py プロジェクト: Arksine/moonraker
 def test_get_boolean_exists(self, test_config: ConfigHelper):
     val = test_config.getboolean("test_bool")
     assert val is True
コード例 #23
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.software_version = self.server.get_app_args().get(
            'software_version')
        self.enable_ufp: bool = config.getboolean('enable_ufp', True)

        # Get webcam settings from config
        self.webcam: Dict[str, Any] = {
            'flipH': config.getboolean('flip_h', False),
            'flipV': config.getboolean('flip_v', False),
            'rotate90': config.getboolean('rotate_90', False),
            'streamUrl': config.get('stream_url', '/webcam/?action=stream'),
            'webcamEnabled': config.getboolean('webcam_enabled', True),
        }

        # Local variables
        self.klippy_apis: APIComp = self.server.lookup_component('klippy_apis')
        self.heaters: Dict[str, Dict[str, Any]] = {}
        self.last_print_stats: Dict[str, Any] = {}

        # Register status update event
        self.server.register_event_handler('server:klippy_ready', self._init)
        self.server.register_event_handler('server:status_update',
                                           self._handle_status_update)

        # Version & Server information
        self.server.register_endpoint('/api/version', ['GET'],
                                      self._get_version,
                                      transports=['http'],
                                      wrap_result=False)
        self.server.register_endpoint('/api/server', ['GET'],
                                      self._get_server,
                                      transports=['http'],
                                      wrap_result=False)

        # Login, User & Settings
        self.server.register_endpoint('/api/login', ['POST'],
                                      self._post_login_user,
                                      transports=['http'],
                                      wrap_result=False)
        self.server.register_endpoint('/api/currentuser', ['GET'],
                                      self._post_login_user,
                                      transports=['http'],
                                      wrap_result=False)
        self.server.register_endpoint('/api/settings', ['GET'],
                                      self._get_settings,
                                      transports=['http'],
                                      wrap_result=False)

        # File operations
        # Note that file upload is handled in file_manager.py
        # TODO: List/info/select/delete files

        # Job operations
        self.server.register_endpoint('/api/job', ['GET'],
                                      self._get_job,
                                      transports=['http'],
                                      wrap_result=False)
        # TODO: start/cancel/restart/pause jobs

        # Printer operations
        self.server.register_endpoint('/api/printer', ['GET'],
                                      self._get_printer,
                                      transports=['http'],
                                      wrap_result=False)
        self.server.register_endpoint('/api/printer/command', ['POST'],
                                      self._post_command,
                                      transports=['http'],
                                      wrap_result=False)
        # TODO: head/tool/bed/chamber specific read/issue

        # Printer profiles
        self.server.register_endpoint('/api/printerprofiles', ['GET'],
                                      self._get_printerprofiles,
                                      transports=['http'],
                                      wrap_result=False)

        # Upload Handlers
        self.server.register_upload_handler(
            "/api/files/local", location_prefix="api/files/moonraker")
        self.server.register_endpoint(
            "/api/files/moonraker/(?P<relative_path>.+)", ['POST'],
            self._select_file,
            transports=['http'],
            wrap_result=False)
コード例 #24
0
ファイル: power.py プロジェクト: Swift-Tester/moonraker
 def __init__(self, config: ConfigHelper):
     super().__init__(config)
     self.initial_state = config.getboolean('initial_state', False)
コード例 #25
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.login_timeout = config.getint('login_timeout', 90)
        self.force_logins = config.getboolean('force_logins', False)
        database: DBComp = self.server.lookup_component('database')
        database.register_local_namespace('authorized_users', forbidden=True)
        self.users = database.wrap_namespace('authorized_users')
        api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None)
        if api_user is None:
            self.api_key = uuid.uuid4().hex
            self.users[API_USER] = {
                'username': API_USER,
                'api_key': self.api_key,
                'created_on': time.time()
            }
        else:
            self.api_key = api_user['api_key']
        host_name, port = self.server.get_host_info()
        self.issuer = f"http://{host_name}:{port}"
        self.public_jwks: Dict[str, Dict[str, Any]] = {}
        for username, user_info in list(self.users.items()):
            if username == API_USER:
                continue
            if 'jwt_secret' in user_info:
                try:
                    priv_key = self._load_private_key(user_info['jwt_secret'])
                    jwk_id = user_info['jwk_id']
                except (self.server.error, KeyError):
                    logging.info("Invalid key found for user, removing")
                    user_info.pop('jwt_secret', None)
                    user_info.pop('jwk_id', None)
                    self.users[username] = user_info
                    continue
                self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key)

        self.trusted_users: Dict[IPAddr, Any] = {}
        self.oneshot_tokens: Dict[str, OneshotToken] = {}
        self.permitted_paths: Set[str] = set()

        # Get allowed cors domains
        self.cors_domains: List[str] = []
        cors_cfg = config.get('cors_domains', "").strip()
        cds = [d.strip() for d in cors_cfg.split('\n') if d.strip()]
        for domain in cds:
            bad_match = re.search(r"^.+\.[^:]*\*", domain)
            if bad_match is not None:
                raise config.error(
                    f"Unsafe CORS Domain '{domain}'.  Wildcards are not"
                    " permitted in the top level domain.")
            self.cors_domains.append(
                domain.replace(".", "\\.").replace("*", ".*"))

        # Get Trusted Clients
        self.trusted_ips: List[IPAddr] = []
        self.trusted_ranges: List[IPNetwork] = []
        self.trusted_domains: List[str] = []
        tcs = config.get('trusted_clients', "")
        trusted_clients = [c.strip() for c in tcs.split('\n') if c.strip()]
        for val in trusted_clients:
            # Check IP address
            try:
                tc = ipaddress.ip_address(val)
            except ValueError:
                pass
            else:
                self.trusted_ips.append(tc)
                continue
            # Check ip network
            try:
                tc = ipaddress.ip_network(val)
            except ValueError:
                pass
            else:
                self.trusted_ranges.append(tc)
                continue
            # Check hostname
            self.trusted_domains.append(val.lower())

        t_clients = "\n".join([str(ip) for ip in self.trusted_ips] +
                              [str(rng) for rng in self.trusted_ranges] +
                              self.trusted_domains)
        c_domains = "\n".join(self.cors_domains)

        logging.info(f"Authorization Configuration Loaded\n"
                     f"Trusted Clients:\n{t_clients}\n"
                     f"CORS Domains:\n{c_domains}")

        self.prune_handler = PeriodicCallback(self._prune_conn_handler,
                                              PRUNE_CHECK_TIME)
        self.prune_handler.start()

        # Register Authorization Endpoints
        self.permitted_paths.add("/server/redirect")
        self.permitted_paths.add("/access/login")
        self.permitted_paths.add("/access/refresh_jwt")
        self.server.register_endpoint("/access/login", ['POST'],
                                      self._handle_login,
                                      protocol=['http'])
        self.server.register_endpoint("/access/logout", ['POST'],
                                      self._handle_logout,
                                      protocol=['http'])
        self.server.register_endpoint("/access/refresh_jwt", ['POST'],
                                      self._handle_refresh_jwt,
                                      protocol=['http'])
        self.server.register_endpoint("/access/user",
                                      ['GET', 'POST', 'DELETE'],
                                      self._handle_user_request,
                                      protocol=['http'])
        self.server.register_endpoint("/access/users/list", ['GET'],
                                      self._handle_list_request,
                                      protocol=['http'])
        self.server.register_endpoint("/access/user/password", ['POST'],
                                      self._handle_password_reset,
                                      protocol=['http'])
        self.server.register_endpoint("/access/api_key", ['GET', 'POST'],
                                      self._handle_apikey_request,
                                      protocol=['http'])
        self.server.register_endpoint("/access/oneshot_token", ['GET'],
                                      self._handle_oneshot_request,
                                      protocol=['http'])
        self.server.register_notification("authorization:user_created")
        self.server.register_notification("authorization:user_deleted")
コード例 #26
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.event_loop = self.server.get_event_loop()
        self.app_config = config.read_supplemental_config(
            SUPPLEMENTAL_CFG_PATH)
        auto_refresh_enabled = config.getboolean('enable_auto_refresh', False)
        self.channel = config.get('channel', "dev")
        if self.channel not in ["dev", "beta"]:
            raise config.error(
                f"Unsupported channel '{self.channel}' in section"
                " [update_manager]")
        self.cmd_helper = CommandHelper(config)
        self.updaters: Dict[str, BaseDeploy] = {}
        if config.getboolean('enable_system_updates', True):
            self.updaters['system'] = PackageDeploy(config, self.cmd_helper)
        if (
            os.path.exists(KLIPPER_DEFAULT_PATH) and
            os.path.exists(KLIPPER_DEFAULT_EXEC)
        ):
            self.updaters['klipper'] = get_deploy_class(KLIPPER_DEFAULT_PATH)(
                self.app_config[f"update_manager klipper"], self.cmd_helper,
                {
                    'channel': self.channel,
                    'path': KLIPPER_DEFAULT_PATH,
                    'executable': KLIPPER_DEFAULT_EXEC
                })
        else:
            self.updaters['klipper'] = BaseDeploy(
                self.app_config[f"update_manager klipper"], self.cmd_helper)
        self.updaters['moonraker'] = get_deploy_class(MOONRAKER_PATH)(
            self.app_config[f"update_manager moonraker"], self.cmd_helper,
            {
                'channel': self.channel,
                'path': MOONRAKER_PATH,
                'executable': sys.executable
            })

        # TODO: The below check may be removed when invalid config options
        # raise a config error.
        if (
            config.get("client_repo", None) is not None or
            config.get('client_path', None) is not None
        ):
            raise config.error(
                "The deprecated 'client_repo' and 'client_path' options\n"
                "have been removed.  See Moonraker's configuration docs\n"
                "for details on client configuration.")
        client_sections = config.get_prefix_sections("update_manager ")
        for section in client_sections:
            cfg = config[section]
            name = section.split()[-1]
            if name in self.updaters:
                raise config.error(f"Client repo {name} already added")
            client_type = cfg.get("type")
            if client_type in ["web", "web_beta"]:
                self.updaters[name] = WebClientDeploy(cfg, self.cmd_helper)
            elif client_type in ["git_repo", "zip", "zip_beta"]:
                path = os.path.expanduser(cfg.get('path'))
                self.updaters[name] = get_deploy_class(path)(
                    cfg, self.cmd_helper)
            else:
                raise config.error(
                    f"Invalid type '{client_type}' for section [{section}]")

        self.cmd_request_lock = asyncio.Lock()
        self.initialized_lock = asyncio.Event()
        self.klippy_identified_evt: Optional[asyncio.Event] = None

        # Auto Status Refresh
        self.last_refresh_time: float = 0
        self.refresh_cb: Optional[PeriodicCallback] = None
        if auto_refresh_enabled:
            self.refresh_cb = PeriodicCallback(
                self._handle_auto_refresh,  # type: ignore
                UPDATE_REFRESH_INTERVAL_MS)

        self.server.register_endpoint(
            "/machine/update/moonraker", ["POST"],
            self._handle_update_request)
        self.server.register_endpoint(
            "/machine/update/klipper", ["POST"],
            self._handle_update_request)
        self.server.register_endpoint(
            "/machine/update/system", ["POST"],
            self._handle_update_request)
        self.server.register_endpoint(
            "/machine/update/client", ["POST"],
            self._handle_update_request)
        self.server.register_endpoint(
            "/machine/update/full", ["POST"],
            self._handle_full_update_request)
        self.server.register_endpoint(
            "/machine/update/status", ["GET"],
            self._handle_status_request)
        self.server.register_endpoint(
            "/machine/update/recover", ["POST"],
            self._handle_repo_recovery)
        self.server.register_notification("update_manager:update_response")
        self.server.register_notification("update_manager:update_refreshed")

        # Register Ready Event
        self.server.register_event_handler(
            "server:klippy_identified", self._set_klipper_repo)
        # Initialize GitHub API Rate Limits and configured updaters
        self.event_loop.register_callback(
            self._initalize_updaters, list(self.updaters.values()))
コード例 #27
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.login_timeout = config.getint('login_timeout', 90)
        self.force_logins = config.getboolean('force_logins', False)
        database: DBComp = self.server.lookup_component('database')
        database.register_local_namespace('authorized_users', forbidden=True)
        self.users = database.wrap_namespace('authorized_users')
        api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None)
        if api_user is None:
            self.api_key = uuid.uuid4().hex
            self.users[API_USER] = {
                'username': API_USER,
                'api_key': self.api_key,
                'created_on': time.time()
            }
        else:
            self.api_key = api_user['api_key']
        hi = self.server.get_host_info()
        self.issuer = f"http://{hi['hostname']}:{hi['port']}"
        self.public_jwks: Dict[str, Dict[str, Any]] = {}
        for username, user_info in list(self.users.items()):
            if username == API_USER:
                # Validate the API User
                for item in ["username", "api_key", "created_on"]:
                    if item not in user_info:
                        self.users[API_USER] = {
                            'username': API_USER,
                            'api_key': self.api_key,
                            'created_on': time.time()
                        }
                        break
                continue
            else:
                # validate created users
                valid = True
                for item in ["username", "password", "salt", "created_on"]:
                    if item not in user_info:
                        logging.info(
                            f"Authorization: User {username} does not "
                            f"contain field {item}, removing")
                        del self.users[username]
                        valid = False
                        break
                if not valid:
                    continue
            # generate jwks for valid users
            if 'jwt_secret' in user_info:
                try:
                    priv_key = self._load_private_key(user_info['jwt_secret'])
                    jwk_id = user_info['jwk_id']
                except (self.server.error, KeyError):
                    logging.info("Invalid key found for user, removing")
                    user_info.pop('jwt_secret', None)
                    user_info.pop('jwk_id', None)
                    self.users[username] = user_info
                    continue
                self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key)

        self.trusted_users: Dict[IPAddr, Any] = {}
        self.oneshot_tokens: Dict[str, OneshotToken] = {}
        self.permitted_paths: Set[str] = set()

        # Get allowed cors domains
        self.cors_domains: List[str] = []
        for domain in config.getlist('cors_domains', []):
            bad_match = re.search(r"^.+\.[^:]*\*", domain)
            if bad_match is not None:
                raise config.error(
                    f"Unsafe CORS Domain '{domain}'.  Wildcards are not"
                    " permitted in the top level domain.")
            if domain.endswith("/"):
                self.server.add_warning(
                    f"[authorization]: Invalid domain '{domain}' in option "
                    "'cors_domains'.  Domain's cannot contain a trailing "
                    "slash.")
            else:
                self.cors_domains.append(
                    domain.replace(".", "\\.").replace("*", ".*"))

        # Get Trusted Clients
        self.trusted_ips: List[IPAddr] = []
        self.trusted_ranges: List[IPNetwork] = []
        self.trusted_domains: List[str] = []
        for val in config.getlist('trusted_clients', []):
            # Check IP address
            try:
                tc = ipaddress.ip_address(val)
            except ValueError:
                pass
            else:
                self.trusted_ips.append(tc)
                continue
            # Check ip network
            try:
                tc = ipaddress.ip_network(val)
            except ValueError as e:
                if "has host bits set" in str(e):
                    self.server.add_warning(
                        f"[authorization]: Invalid CIDR expression '{val}' "
                        "in option 'trusted_clients'")
                    continue
                pass
            else:
                self.trusted_ranges.append(tc)
                continue
            # Check hostname
            match = re.match(r"([a-z0-9]+(-[a-z0-9]+)*\.?)+[a-z]{2,}$", val)
            if match is not None:
                self.trusted_domains.append(val.lower())
            else:
                self.server.add_warning(
                    f"[authorization]: Invalid domain name '{val}' "
                    "in option 'trusted_clients'")

        t_clients = "\n".join([str(ip) for ip in self.trusted_ips] +
                              [str(rng) for rng in self.trusted_ranges] +
                              self.trusted_domains)
        c_domains = "\n".join(self.cors_domains)

        logging.info(f"Authorization Configuration Loaded\n"
                     f"Trusted Clients:\n{t_clients}\n"
                     f"CORS Domains:\n{c_domains}")

        eventloop = self.server.get_event_loop()
        self.prune_timer = eventloop.register_timer(self._prune_conn_handler)

        # Register Authorization Endpoints
        self.permitted_paths.add("/server/redirect")
        self.permitted_paths.add("/access/login")
        self.permitted_paths.add("/access/refresh_jwt")
        self.server.register_endpoint("/access/login", ['POST'],
                                      self._handle_login,
                                      transports=['http'])
        self.server.register_endpoint("/access/logout", ['POST'],
                                      self._handle_logout,
                                      transports=['http'])
        self.server.register_endpoint("/access/refresh_jwt", ['POST'],
                                      self._handle_refresh_jwt,
                                      transports=['http'])
        self.server.register_endpoint("/access/user",
                                      ['GET', 'POST', 'DELETE'],
                                      self._handle_user_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/users/list", ['GET'],
                                      self._handle_list_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/user/password", ['POST'],
                                      self._handle_password_reset,
                                      transports=['http'])
        self.server.register_endpoint("/access/api_key", ['GET', 'POST'],
                                      self._handle_apikey_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/oneshot_token", ['GET'],
                                      self._handle_oneshot_request,
                                      transports=['http'])
        self.server.register_notification("authorization:user_created")
        self.server.register_notification("authorization:user_deleted")