def test_get_list_deprecate(self, test_config: ConfigHelper): server = test_config.get_server() test_config.getlist("test_list", deprecate=True) expected = ( f"[test_options]: Option 'test_list' is " "deprecated, see the configuration documention " "at https://moonraker.readthedocs.io/en/latest/configuration") assert expected in server.warnings
def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper) -> None: super().__init__(config, cmd_helper, prefix="Web Client") self.repo = config.get('repo').strip().strip("/") self.owner = self.repo.split("/", 1)[0] self.path = pathlib.Path(config.get("path")).expanduser().resolve() self.type = config.get('type') self.channel = "stable" if self.type == "web" else "beta" self.persistent_files: List[str] = [] pfiles = config.getlist('persistent_files', None) if pfiles is not None: self.persistent_files = [pf.strip("/") for pf in pfiles] if ".version" in self.persistent_files: raise config.error( "Invalid value for option 'persistent_files': " "'.version' can not be persistent") storage = self._load_storage() self.version: str = storage.get('version', "?") self.remote_version: str = storage.get('remote_version', "?") dl_info: List[Any] = storage.get('dl_info', ["?", "?", 0]) self.dl_info: Tuple[str, str, int] = cast(Tuple[str, str, int], tuple(dl_info)) self.refresh_evt: Optional[asyncio.Event] = None self.mutex: asyncio.Lock = asyncio.Lock() logging.info(f"\nInitializing Client Updater: '{self.name}'," f"\nChannel: {self.channel}" f"\npath: {self.path}")
def __init__(self, config: ConfigHelper) -> None: self.config = config 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.apprise = apprise.Apprise() self.warned = False self.attach_requires_file_system_check = True self.attach = config.get("attach", None) if self.attach is None or \ (self.attach.startswith("http://") or self.attach.startswith("https://")): self.attach_requires_file_system_check = False url_template = config.gettemplate('url') self.url = url_template.render() if len(self.url) < 2: raise config.error(f"Invalid url for: {config.get_name()}") self.title = config.gettemplate('title', None) self.body = config.gettemplate("body", None) self.events: List[str] = config.getlist("events", separator=",") self.apprise.add(self.url)
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" )
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" }
def test_get_list_default(self, test_config: ConfigHelper): assert test_config.getlist("invalid_option", None) is None
def test_get_list_fail(self, test_config: ConfigHelper): with pytest.raises(ConfigError): test_config.getlist("invalid_option")
def test_get_list_exists(self, test_config: ConfigHelper): val = test_config.getlist("test_list") assert val == ["one", "two", "three"]
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")
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)