def _validate_generic_container_options(container_options, force_options=False): validators = [ validate.NamesIn( GENERIC_CONTAINER_OPTIONS, option_type="container", **validate.set_warning(report_codes.FORCE_OPTIONS, force_options), ), validate.IsRequiredAll(["image"], option_type="container"), validate.ValueNotEmpty("image", "image name"), validate.ValueNonnegativeInteger("masters"), validate.ValueNonnegativeInteger("promoted-max"), validate.MutuallyExclusive( ["masters", "promoted-max"], option_type="container", ), validate.ValuePositiveInteger("replicas"), validate.ValuePositiveInteger("replicas-per-host"), ] deprecation_reports = [] if "masters" in container_options: deprecation_reports.append( ReportItem.warning( reports.messages.DeprecatedOption( "masters", ["promoted-max"], "container", ))) return (validate.ValidatorAll(validators).validate(container_options) + deprecation_reports)
def _validate_port_map_list(options_list, id_provider, force_options): kwargs = validate.set_warning(report_codes.FORCE_OPTIONS, force_options) option_type = "port-map" validators = [ validate.NamesIn(PORT_MAP_OPTIONS, option_type=option_type, **kwargs), validate.ValueId("id", option_name_for_report="port-map id", id_provider=id_provider), validate.DependsOnOption( ["internal-port"], "port", option_type=option_type, prerequisite_type=option_type, ), validate.IsRequiredSome(["port", "range"], option_type=option_type), validate.MutuallyExclusive(["port", "range"], option_type=option_type), validate.ValuePortNumber("port"), validate.ValuePortNumber("internal-port"), validate.ValuePortRange("range", **kwargs), ] validator_all = validate.ValidatorAll(validators) report_list = [] for options in options_list: report_list.extend(validator_all.validate(options)) return report_list
def test_return_error_on_not_allowed_and_banned_names_forced(self): code = "force_code" assert_report_item_list_equal( validate.NamesIn( ["a", "b"], banned_name_list=["x", "y", "z"], code_for_warning=code, produce_warning=True, ).validate({"x": "X", "a": "A", "z": "Z", "c": "C", "d": "D"}), [ fixture.warn( report_codes.INVALID_OPTIONS, option_names=["c", "d"], allowed=["a", "b"], option_type=None, allowed_patterns=[], ), fixture.error( report_codes.INVALID_OPTIONS, option_names=["x", "z"], allowed=["a", "b"], option_type=None, allowed_patterns=[], ), ], )
def _validate_storage_map_list(options_list, id_provider, force_options): kwargs = validate.set_warning(report_codes.FORCE_OPTIONS, force_options) option_type = "storage-map" validators = [ validate.NamesIn(STORAGE_MAP_OPTIONS, option_type=option_type, **kwargs), validate.ValueId( "id", option_name_for_report="storage-map id", id_provider=id_provider, ), validate.IsRequiredSome( ["source-dir", "source-dir-root"], option_type=option_type, ), validate.MutuallyExclusive( ["source-dir", "source-dir-root"], option_type=option_type, ), validate.IsRequiredAll(["target-dir"], option_type=option_type), ] validator_all = validate.ValidatorAll(validators) report_list = [] for options in options_list: report_list.extend(validator_all.validate(options)) return report_list
def validate_operation_list(operation_list, allowed_operation_name_list, allow_invalid=False): kwargs = validate.set_warning(report_codes.FORCE_OPTIONS, allow_invalid) option_type = "resource operation" validators = [ validate.NamesIn(ATTRIBUTES, option_type=option_type), validate.IsRequiredAll(["name"], option_type=option_type), validate.ValueIn( "name", allowed_operation_name_list, option_name_for_report="operation name", **kwargs, ), validate.ValueIn("role", RESOURCE_ROLES), validate.ValueIn("on-fail", ON_FAIL_VALUES), validate.ValueIn("record-pending", BOOLEAN_VALUES), validate.ValueIn("enabled", BOOLEAN_VALUES), validate.MutuallyExclusive(["interval-origin", "start-delay"], option_type=option_type), validate.ValueId("id", option_name_for_report="operation id"), ] validator_all = validate.ValidatorAll(validators) report_list = [] for operation in operation_list: report_list.extend(validator_all.validate(operation)) return report_list
def test_return_error_on_not_allowed_and_banned_names_forced(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b"], banned_name_list=["x", "y", "z"], severity=reports.item.ReportItemSeverity.warning(), ).validate({ "x": "X", "a": "A", "z": "Z", "c": "C", "d": "D" }), [ fixture.warn( reports.codes.INVALID_OPTIONS, option_names=["c", "d"], allowed=["a", "b"], option_type=None, allowed_patterns=[], ), fixture.error( reports.codes.INVALID_OPTIONS, option_names=["x", "z"], allowed=["a", "b"], option_type=None, allowed_patterns=[], ), ], )
def initialize_block_devices(lib_env, device_list, option_dict): """ Initialize SBD devices in device_list with options_dict. lib_env -- LibraryEnvironment device_list -- list of strings option_dict -- dictionary """ report_item_list = [] if not device_list: report_item_list.append( reports.required_options_are_missing(["device"])) supported_options = sbd.DEVICE_INITIALIZATION_OPTIONS_MAPPING.keys() report_item_list += ( validate.NamesIn(supported_options).validate(option_dict)) report_item_list += validate.ValidatorAll([ validate.ValueNonnegativeInteger(key) for key in supported_options ]).validate(option_dict) if lib_env.report_processor.report_list(report_item_list).has_errors: raise LibraryError() sbd.initialize_block_devices(lib_env.report_processor, lib_env.cmd_runner(), device_list, option_dict)
def _validate_sbd_options(sbd_config, allow_unknown_opts=False, allow_invalid_option_values=False): """ Validate user SBD configuration. Options 'SBD_WATCHDOG_DEV' and 'SBD_OPTS' are restricted. Returns list of ReportItem sbd_config -- dictionary in format: <SBD config option>: <value> allow_unknown_opts -- if True, accept also unknown options. """ validators = [ validate.NamesIn( ALLOWED_SBD_OPTION_LIST, banned_name_list=UNSUPPORTED_SBD_OPTION_LIST, **validate.set_warning(report_codes.FORCE_OPTIONS, allow_unknown_opts), ), validate.ValueNonnegativeInteger("SBD_WATCHDOG_TIMEOUT"), validate.ValueIn( "SBD_TIMEOUT_ACTION", TIMEOUT_ACTION_ALLOWED_VALUE_LIST, **validate.set_warning(report_codes.FORCE_OPTIONS, allow_invalid_option_values), ), ] return validate.ValidatorAll(validators).validate(sbd_config)
def validate_parameters_create( self, parameters, force=False, # TODO remove this argument, see pcs.lib.cib.commands.remote_node.create # for details do_not_report_instance_attribute_server_exists=False, ): # This is just a basic validation checking that required parameters are # set and all set parameters are known to an agent. Missing checks are: # 1. values checks - if a param is an integer, then "abc" is not valid # 2. warnings should be emitted when a deprecated param is set # 3. errors should be emitted when a deprecated parameter and a # parameter obsoleting it are set at the same time # 4. possibly some other checks # All of these have been missing in pcs since ever (ad 1. agents have # never provided enough info for us to do such validations, ad 2. and # 3. there were no deprecated parameters before). The checks should be # implemented in agents themselves, so I'm not adding them now either. report_items = [] # report unknown parameters report_items.extend( validate.NamesIn( {param["name"] for param in self.get_parameters()}, option_type=self._agent_type_label, **validate.set_warning(reports.codes.FORCE_OPTIONS, force), ).validate(parameters)) # TODO remove this "if", see pcs.lib.cib.commands.remote_node.create # for details if do_not_report_instance_attribute_server_exists: for report_item in report_items: if isinstance(report_item, ReportItem) and isinstance( report_item.message, reports.messages.InvalidOptions): report_msg = cast(reports.messages.InvalidOptions, report_item.message) report_item.message = reports.messages.InvalidOptions( report_msg.option_names, sorted([ value for value in report_msg.allowed if value != "server" ]), report_msg.option_type, report_msg.allowed_patterns, ) # report missing required parameters missing_parameters = self._find_missing_required_parameters(parameters) if missing_parameters: report_items.append( ReportItem( severity=self._validate_report_severity(force), message=reports.messages.RequiredOptionsAreMissing( sorted(missing_parameters), self._agent_type_label, ), )) return report_items
def _validate_network_options_update(bundle_el, network_el, options, force_options): report_list = [] inner_primitive = get_inner_resource(bundle_el) if (inner_primitive is not None and not _is_pcmk_remote_acccessible_after_update(network_el, options)): report_list.append( reports.get_problem_creator( report_codes.FORCE_OPTIONS, force_options)(reports.resource_in_bundle_not_accessible, bundle_el.get("id"), inner_primitive.get("id"))) kwargs = validate.set_warning(report_codes.FORCE_OPTIONS, force_options) validators_optional_options = [ # TODO add validators for other keys (ip-range-start - IPv4) validate.ValuePortNumber("control-port"), # Leaving a possibility to force this validation for the case pacemaker # starts supporting IPv6 or other format of the netmask. ValueHostNetmask("host-netmask", **kwargs), ] for val in validators_optional_options: val.empty_string_valid = True validators = [ validate.NamesIn( # allow to remove options even if they are not allowed NETWORK_OPTIONS | _options_to_remove(options), option_type="network", **kwargs) ] + validators_optional_options return (report_list + validate.ValidatorAll(validators).validate(options))
def validate_parameters_update(self, current_parameters, new_parameters, force=False): # This is just a basic validation checking that required parameters are # set and all set parameters are known to an agent. Missing checks are: # 1. values checks - if a param is an integer, then "abc" is not valid # 2. warnings should be emitted when a deprecated param is set # 3. errors should be emitted when a deprecated parameter and a # parameter obsoleting it are set at the same time # 4. possibly some other checks # All of these have been missing in pcs since ever (ad 1. agents have # never provided enough info for us to do such validations, ad 2. and # 3. there were no deprecated parameters before). The checks should be # implemented in agents themselves, so I'm not adding them now either. report_items = [] # get resulting set of agent's parameters final_parameters = dict(current_parameters) for name, value in new_parameters.items(): if value: final_parameters[name] = value else: if name in final_parameters: del final_parameters[name] # report unknown parameters report_items.extend( validate.NamesIn( {param["name"] for param in self.get_parameters()}, option_type=self._agent_type_label, severity=reports.item.get_severity(reports.codes.FORCE_OPTIONS, force), ).validate( # Do not report unknown parameters already set in the CIB. They # have been reported already when the were added to the CIB. { name: value for name, value in new_parameters.items() if name not in current_parameters })) # report missing or removed required parameters missing_parameters = self._find_missing_required_parameters( final_parameters) if missing_parameters: report_items.append( ReportItem( severity=self._validate_report_severity(force), message=reports.messages.RequiredOptionsAreMissing( sorted(missing_parameters), self._agent_type_label, ), )) return report_items
def _validate_generic_container_options_update(container_el, options, force_options): validators_optional_options = [ validate.ValueNonnegativeInteger("masters"), validate.ValueNonnegativeInteger("promoted-max"), validate.ValuePositiveInteger("replicas"), validate.ValuePositiveInteger("replicas-per-host"), ] for val in validators_optional_options: val.empty_string_valid = True validators = [ validate.NamesIn( # allow to remove options even if they are not allowed GENERIC_CONTAINER_OPTIONS | _options_to_remove(options), option_type="container", **validate.set_warning(report_codes.FORCE_OPTIONS, force_options)), # image is a mandatory attribute and cannot be removed validate.ValueNotEmpty("image", "image name") ] + validators_optional_options # CIB does not allow both to be set. Deleting both is not a problem, # though. Deleting one while setting another also works and is further # checked bellow. if not (options.get("masters", "") == "" or options.get("promoted-max", "") == ""): validators.append( validate.MutuallyExclusive( ["masters", "promoted-max"], option_type="container", )) deprecation_reports = [] if options.get("masters"): # If the user wants to delete the masters option, do not report it is # deprecated. They may be removing it because they just found out it is # deprecated. deprecation_reports.append( reports.deprecated_option("masters", ["promoted-max"], "container", severity=ReportItemSeverity.WARNING)) # Do not allow to set masters if promoted-max is set unless promoted-max is # going to be removed now. Do the same check also the other way around. CIB # only allows one of them to be set. if (options.get("masters") and container_el.get("promoted-max") and options.get("promoted-max") != ""): deprecation_reports.append( reports.prerequisite_option_must_not_be_set( "masters", "promoted-max", "container", "container")) if (options.get("promoted-max") and container_el.get("masters") and options.get("masters") != ""): deprecation_reports.append( reports.prerequisite_option_must_not_be_set( "promoted-max", "masters", "container", "container")) return (validate.ValidatorAll(validators).validate(options) + deprecation_reports)
def _validate_network_options_new(options, force_options): kwargs = validate.set_warning(report_codes.FORCE_OPTIONS, force_options) validators = [ # TODO add validators for other keys (ip-range-start - IPv4) validate.NamesIn(NETWORK_OPTIONS, option_type="network", **kwargs), validate.ValuePortNumber("control-port"), # Leaving a possibility to force this validation for the case pacemaker # starts supporting IPv6 or other format of the netmask. ValueHostNetmask("host-netmask", **kwargs), ] return validate.ValidatorAll(validators).validate(options)
def _validate_ticket_options(options, allow_unknown_options): validator_list = ([ validate.NamesIn(constants.TICKET_KEYS, option_type="booth ticket", banned_name_list=constants.GLOBAL_KEYS, **validate.set_warning(report_codes.FORCE_OPTIONS, allow_unknown_options)), ] + [validate.ValueNotEmpty(option, None) for option in options]) normalized_options = validate.values_to_pairs( options, lambda key, value: value.strip()) return validate.ValidatorAll(validator_list).validate(normalized_options)
def validate_set_as_guest(tree, existing_nodes_names, existing_nodes_addrs, node_name, options): validator_list = [ validate.NamesIn(GUEST_OPTIONS, option_type="guest"), validate.ValueTimeInterval("remote-connect-timeout"), validate.ValuePortNumber("remote-port"), ] return (validate.ValidatorAll(validator_list).validate(options) + validate.ValueNotEmpty("node name", None).validate( {"node name": node_name.strip()}) + validate_conflicts(tree, existing_nodes_names, existing_nodes_addrs, node_name, options))
def validate(self, force_options: bool = False) -> reports.ReportItemList: report_list: reports.ReportItemList = [] # Nvpair dict is intentionally not validated: it may contain any keys # and values. This can change in the future and then we add a # validation. Until then there is really nothing to validate there. # validate nvset options validators = [ validate.NamesIn( ("id", "score"), severity=reports.item.get_severity(reports.codes.FORCE_OPTIONS, force_options), ), # with id_provider it validates that the id is available as well validate.ValueId("id", option_name_for_report="id", id_provider=self._id_provider), validate.ValueScore("score"), ] report_list.extend( validate.ValidatorAll(validators).validate(self._nvset_options)) # parse and validate rule # TODO write and call parsed rule validation and cleanup and tests if self._nvset_rule: try: # Allow flags are set to True always, the parsed rule tree is # checked in the validator instead. That gives us better error # messages, such as "op expression cannot be used in this # context" instead of a universal "parse error". self._nvset_rule_parsed = parse_rule(self._nvset_rule, allow_rsc_expr=True, allow_op_expr=True) report_list.extend( RuleValidator( self._nvset_rule_parsed, allow_rsc_expr=self._allow_rsc_expr, allow_op_expr=self._allow_op_expr, ).get_reports()) except RuleParseError as e: report_list.append( reports.ReportItem.error( reports.messages.RuleExpressionParseError( e.rule_string, e.msg, e.rule_line, e.lineno, e.colno, e.pos, ))) return report_list
def get_validators_allowed_parameters( self, force: bool = False) -> List[validate.ValidatorInterface]: """ Return validators checking for specified parameters names force -- if True, validators produce a warning instead of an error """ return [ validate.NamesIn( {param.name for param in self.metadata.parameters}, self._validator_option_type, severity=reports.item.get_severity(reports.codes.FORCE, force), ) ]
def test_return_error_on_not_allowed_and_banned_names(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b"], banned_name_list=["x", "y", "z"] ).validate({"x": "X", "a": "A", "z": "Z", "c": "C"}), [ fixture.error( report_codes.INVALID_OPTIONS, option_names=["c", "x", "z"], allowed=["a", "b"], option_type=None, allowed_patterns=[], ) ], )
def test_return_error_with_allowed_patterns(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b", "c"], allowed_option_patterns=["pattern"] ).validate({"x": "X", "y": "Y"}), [ fixture.error( report_codes.INVALID_OPTIONS, option_names=["x", "y"], allowed=["a", "b", "c"], option_type=None, allowed_patterns=["pattern"], ) ], )
def _validate_ticket_options(options, allow_unknown_options): validator_list = [ validate.NamesIn( constants.TICKET_KEYS, option_type="booth ticket", banned_name_list=constants.GLOBAL_KEYS, severity=report.item.get_severity( report_codes.FORCE, allow_unknown_options ), ), ] + [validate.ValueNotEmpty(option, None) for option in options] normalized_options = validate.values_to_pairs( options, lambda key, value: value.strip() ) return validate.ValidatorAll(validator_list).validate(normalized_options)
def validate(self, force_options: bool = False) -> reports.ReportItemList: report_list: reports.ReportItemList = [] # Nvpair dict is intentionally not validated: it may contain any keys # and values. This can change in the future and then we add a # validation. Until then there is really nothing to validate there. # validate nvset options validators = [ validate.NamesIn( ("id", "score"), severity=reports.item.get_severity(reports.codes.FORCE, force_options), ), # with id_provider it validates that the id is available as well validate.ValueId("id", option_name_for_report="id", id_provider=self._id_provider), validate.ValueScore("score"), ] report_list.extend( validate.ValidatorAll(validators).validate(self._nvset_options)) # parse and validate rule if self._nvset_rule: try: self._nvset_rule_parsed = parse_rule(self._nvset_rule) report_list.extend( RuleValidator( self._nvset_rule_parsed, allow_rsc_expr=self._allow_rsc_expr, allow_op_expr=self._allow_op_expr, allow_node_attr_expr=self._allow_node_attr_expr, ).get_reports()) except RuleParseError as e: report_list.append( reports.ReportItem.error( reports.messages.RuleExpressionParseError( e.rule_string, e.msg, e.rule_line, e.lineno, e.colno, e.pos, ))) return report_list
def test_return_error_on_not_allowed_names_without_force_code(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b", "c"], # does now work without code_for_warning produce_warning=True, ).validate({"x": "X", "y": "Y"}), [ fixture.warn( report_codes.INVALID_OPTIONS, option_names=["x", "y"], allowed=["a", "b", "c"], option_type=None, allowed_patterns=[], ) ], )
def _validate_ticket_options(options, allow_unknown_options): severity = reports.item.get_severity(reports.codes.FORCE, allow_unknown_options) validator_list = ([ validate.NamesIn( constants.TICKET_KEYS, option_type="booth ticket", banned_name_list=constants.GLOBAL_KEYS, severity=severity, ), ] + [ validate.ValueNotEmpty(option, None) for option in options if option != "mode" ] + [validate.ValueIn("mode", ["auto", "manual"], severity=severity)]) normalized_options = validate.values_to_pairs( options, lambda key, value: value.strip()) return validate.ValidatorAll(validator_list).validate(normalized_options)
def test_return_error_on_not_allowed_names(self): assert_report_item_list_equal( validate.NamesIn(["a", "b", "c"], option_type="option").validate({ "x": "X", "y": "Y" }), [ fixture.error( reports.codes.INVALID_OPTIONS, option_names=["x", "y"], allowed=["a", "b", "c"], option_type="option", allowed_patterns=[], ) ], )
def test_return_warning_on_not_allowed_names(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b", "c"], option_type="some option", code_for_warning="FORCE_CODE", produce_warning=True, ).validate({"x": "X", "y": "Y"}), [ fixture.warn( report_codes.INVALID_OPTIONS, option_names=["x", "y"], allowed=["a", "b", "c"], option_type="some option", allowed_patterns=[], ) ], )
def _validate_options(options) -> reports.ReportItemList: # Pacemaker does not care currently about meaningfulness for concrete # constraint, so we use all attribs. validators = [ validate.NamesIn(_ATTRIBUTES, option_type="set"), validate.ValueIn("action", const.PCMK_ACTIONS), validate.ValueIn("require-all", _BOOLEAN_VALUES), validate.ValueIn("role", const.PCMK_ROLES), validate.ValueIn("sequential", _BOOLEAN_VALUES), validate.ValueDeprecated( "role", { const.PCMK_ROLE_PROMOTED_LEGACY: const.PCMK_ROLE_PROMOTED, const.PCMK_ROLE_UNPROMOTED_LEGACY: const.PCMK_ROLE_UNPROMOTED, }, reports.ReportItemSeverity.deprecation(), ), ] return validate.ValidatorAll(validators).validate(options)
def test_collect_all_errors_from_specifications(self): assert_report_item_list_equal( validate.ValidatorAll([ validate.NamesIn(["x", "y"]), validate.MutuallyExclusive(["x", "y"]), validate.ValuePositiveInteger("x"), validate.ValueIn("y", ["a", "b"]), ]).validate({ "x": "abcd", "y": "defg", "z": "hijk", }), [ fixture.error( reports.codes.INVALID_OPTIONS, option_names=["z"], option_type=None, allowed=["x", "y"], allowed_patterns=[], ), fixture.error( reports.codes.MUTUALLY_EXCLUSIVE_OPTIONS, option_names=["x", "y"], option_type=None, ), fixture.error( reports.codes.INVALID_OPTION_VALUE, option_value="abcd", option_name="x", allowed_values="a positive integer", cannot_be_empty=False, forbidden_characters=None, ), fixture.error( reports.codes.INVALID_OPTION_VALUE, option_value="defg", option_name="y", allowed_values=["a", "b"], cannot_be_empty=False, forbidden_characters=None, ), ], )
def _validate_network_options_update( bundle_el, network_el, options, force_options ): report_list = [] inner_primitive = get_inner_resource(bundle_el) if ( inner_primitive is not None and not _is_pcmk_remote_accessible_after_update(network_el, options) ): report_list.append( ReportItem( severity=reports.item.get_severity( reports.codes.FORCE, force_options, ), message=reports.messages.ResourceInBundleNotAccessible( bundle_el.get("id"), inner_primitive.get("id"), ), ) ) severity = reports.item.get_severity(reports.codes.FORCE, force_options) validators_optional_options = [ # TODO add validators for other keys (ip-range-start - IPv4) validate.ValuePortNumber("control-port"), # Leaving a possibility to force this validation for the case pacemaker # starts supporting IPv6 or other format of the netmask. ValueHostNetmask("host-netmask", severity=severity), ] for val in validators_optional_options: val.empty_string_valid = True validators = [ validate.NamesIn( # allow to remove options even if they are not allowed NETWORK_OPTIONS | _options_to_remove(options), option_type="network", severity=severity, ) ] + validators_optional_options return report_list + validate.ValidatorAll(validators).validate(options)
def _validate_container(container_type, container_options, force_options=False): if not container_type in GENERIC_CONTAINER_TYPES: return [ reports.invalid_option_value( "container type", container_type, GENERIC_CONTAINER_TYPES, ) ] validators = [ validate.NamesIn( GENERIC_CONTAINER_OPTIONS, option_type="container", **validate.set_warning(report_codes.FORCE_OPTIONS, force_options) ), validate.IsRequiredAll(["image"], option_type="container"), validate.ValueNotEmpty("image", "image name"), validate.ValueNonnegativeInteger("masters"), validate.ValueNonnegativeInteger("promoted-max"), validate.MutuallyExclusive( ["masters", "promoted-max"], option_type="container", ), validate.ValuePositiveInteger("replicas"), validate.ValuePositiveInteger("replicas-per-host"), ] deprecation_reports = [] if "masters" in container_options: deprecation_reports.append( reports.deprecated_option( "masters", ["promoted-max"], "container", severity=ReportItemSeverity.WARNING ) ) return ( validate.ValidatorAll(validators).validate(container_options) + deprecation_reports )
def test_return_warning_on_not_allowed_names(self): assert_report_item_list_equal( validate.NamesIn( ["a", "b", "c"], option_type="some option", severity=reports.item.ReportItemSeverity.warning(), ).validate({ "x": "X", "y": "Y" }), [ fixture.warn( reports.codes.INVALID_OPTIONS, option_names=["x", "y"], allowed=["a", "b", "c"], option_type="some option", allowed_patterns=[], ) ], )