def _validate_and_clean_url(self, param_name, param): """Validate and clean URL input.""" if param.get('allow_list', False): value = param['value'].split(',') else: value = [param['value']] for url in value: # Django's validator requires a protocol but we accept just a # hostname or IP address. try: ip = ip_address(url) except ValueError: if '://' not in url: url = 'http://%s' % url else: if ip.version == 4: url = 'http://%s' % url else: url = 'http://[%s]' % url # Allow all schemes supported by curl(used with the network # validation test) + icmp. try: validate_url(url, schemes=[ 'icmp', 'file', 'ftp', 'ftps', 'gopher', 'http', 'https', 'imap', 'imaps', 'ldap', 'ldaps', 'pop3', 'pop3s', 'rtmp', 'rtsp', 'scp', 'sftp', 'smb', 'smbs', 'smtp', 'smtps', 'telnet', 'tftp' ]) except ValidationError: set_form_error(self, param_name, 'Invalid URL')
def _clean_disabled_boot_architectures(self, cleaned_data): disabled_boot_architectures = cleaned_data.get( "disabled_boot_architectures") if disabled_boot_architectures is None: return cleaned_data disabled_arches = [] for disabled_arch in disabled_boot_architectures: disabled_arches += disabled_arch.split(" ") cleaned_arches = set() octet_to_boot_method = { boot_method.arch_octet: boot_method for _, boot_method in BootMethodRegistry if boot_method.arch_octet } for disabled_arch in disabled_arches: boot_method = BootMethodRegistry.get_item( disabled_arch, octet_to_boot_method.get(disabled_arch.replace("0x", "00:")), ) if boot_method is None or (not boot_method.arch_octet and not boot_method.path_prefix_http): set_form_error( self, "disabled_boot_architectures", f"Unknown boot architecture {disabled_arch}", ) else: cleaned_arches.add(boot_method.name) cleaned_data["disabled_boot_architectures"] = list(cleaned_arches) return cleaned_data
def _validate_results(self, results={}): valid = True if isinstance(results, list): for result in results: if not isinstance(result, str): set_form_error( self, 'results', 'Each result in a result list must be a string.') valid = False elif isinstance(results, dict): for result in results.values(): if not isinstance(result, dict): set_form_error( self, 'results', 'Each result in a result dictionary must be a ' 'dictionary.') elif 'title' not in result: set_form_error( self, 'results', 'title must be included in a result dictionary.') valid = False else: for key in ['title', 'description']: if key in result and not isinstance(result[key], str): set_form_error( self, 'results', '%s must be a string.' % key) valid = False else: set_form_error( self, 'results', 'results must be a list of strings or a dictionary of ' 'dictionaries.') valid = False return valid
def _validate_and_clean_storage(self, param_name, param, result_params, ret): """Validate and clean storage input.""" value = param['value'] for i in value.split(','): if ':' in i: # Allow users to specify a disk using the model and serial. model, serial = i.split(':') try: bd = self._node.physicalblockdevice_set.get(model=model, serial=serial) except ObjectDoesNotExist: pass else: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name][ 'value'] = self._blockdevice_to_dict(bd) ret.append(clean_param) continue qs = self._node.physicalblockdevice_set.filter( Q(name=i) | Q(name=os.path.basename(i)) | Q(model=i) | Q(serial=i) | Q(tags__overlap=[i])) if len(qs) == 0: set_form_error( self, param_name, "Unknown storage device for %s(%s)" % (self._node.fqdn, self._node.system_id)) continue for bd in qs: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name]['value'] = self._blockdevice_to_dict( bd) ret.append(clean_param)
def clean(self): """Validate the lv_size.""" cleaned_data = super().clean() lv_size = self.get_lv_size() if lv_size is not None: root_size = self.get_root_size() if root_size is None: root_size = (self.boot_disk.size - EFI_PARTITION_SIZE - self.get_boot_size()) if is_percentage(lv_size): lv_size = calculate_size_from_percentage(root_size, lv_size) if lv_size < MIN_ROOT_PARTITION_SIZE: set_form_error( self, "lv_size", "Size is too small. Minimum size is %s." % MIN_ROOT_PARTITION_SIZE, ) if lv_size > root_size: set_form_error( self, "lv_size", "Size is too large. Maximum size is %s." % root_size, ) cleaned_data["lv_size"] = lv_size return cleaned_data
def clean_arch(self): """Turn `arch` parameter into a list of architecture names. Even though `arch` is a single-value field, it turns into a list during cleaning. The architecture parameter may be a wildcard. """ # Import list_all_usable_architectures as part of its module, not # directly, so that patch_usable_architectures can easily patch it # for testing purposes. usable_architectures = maasserver_forms.list_all_usable_architectures() architecture_wildcards = get_architecture_wildcards( usable_architectures) value = self.cleaned_data[self.get_field_name("arch")] if value: if value in usable_architectures: # Full 'arch/subarch' specified directly. return [value] elif value in architecture_wildcards: # Try to expand 'arch' to all available 'arch/subarch' # matches. return architecture_wildcards[value] set_form_error( self, self.get_field_name("arch"), "Architecture not recognised.", ) return None
def clean(self): cleaned_data = super().clean() power_type = self.cleaned_data.get("type") if not self.drivers: set_form_error( self, "type", "No rack controllers are connected, unable to validate.", ) elif not self.is_new and self.instance.power_type != power_type: set_form_error( self, "type", "Cannot change the type of a pod. Delete and re-create the " "pod with a different type.", ) should_generate_cert = (power_type == "lxd" and not cleaned_data.get("certificate") and not cleaned_data.get("key")) if should_generate_cert: pod_name = cleaned_data.get("name") cert = generate_certificate(get_maas_client_cn(pod_name)) cleaned_data["certificate"] = cert.certificate_pem() cleaned_data["key"] = cert.private_key_pem() return cleaned_data
def clean(self): cleaned_data = super().clean() if "space" in self.data: set_form_error( self, "space", "Spaces may no longer be set on subnets. Set the space on the " "underlying VLAN.", ) # The default value for 'allow_dns' is True. if "allow_dns" not in self.data: cleaned_data["allow_dns"] = True # The default value for 'allow_proxy' is True. if "allow_proxy" not in self.data: cleaned_data["allow_proxy"] = True # The default value for 'managed' is True. if "managed" not in self.data: cleaned_data["managed"] = True # The ArrayField form has a bug which leaves out the first entry. if "dns_servers" in self.data and self.data["dns_servers"] != "": cleaned_data["dns_servers"] = self.data.getlist("dns_servers") cleaned_data = self._clean_name(cleaned_data) cleaned_data = self._clean_dns_servers(cleaned_data) cleaned_data = self._clean_disabled_boot_architectures(cleaned_data) if self.instance.id is None: # We only allow the helpers when creating. When updating we require # the VLAN specifically. This is because we cannot make a correct # determination on what should be done in this case. cleaned_data = self._clean_vlan(cleaned_data) return cleaned_data
def clean_for_hardware(self): """Convert from JSON and validate for_hardware input.""" if self.cleaned_data["for_hardware"] == "": return self.instance.for_hardware try: for_hardware = json.loads(self.cleaned_data["for_hardware"]) except JSONDecodeError: for_hardware = self.cleaned_data["for_hardware"] if isinstance(for_hardware, str): for_hardware = for_hardware.split(",") if not isinstance(for_hardware, list): set_form_error(self, "for_hardware", "Must be a list or string") return regex = re.compile( r"^modalias:.+|pci:[\da-f]{4}:[\da-f]{4}|" r"usb:[\da-f]{4}:[\da-f]{4}|" r"system_vendor:.*|" r"system_product:.*|" r"system_version:.*|" r"mainboard_vendor:.*|" r"mainboard_product:.*$", re.I, ) for hw_id in for_hardware: if regex.search(hw_id) is None: set_form_error( self, "for_hardware", "Hardware identifier '%s' must be a modalias, PCI ID, " "USB ID, system vendor, system product, system version, " "mainboard vendor, or mainboard product." % hw_id, ) return for_hardware
def _validate_and_clean_choices(self, param_name, param): # Support a Django choice list or a string list choices = [ choice if isinstance(choice, str) else choice[0] for choice in param["choices"] ] if param["value"] not in choices: set_form_error(self, param_name, f"Invalid choice \"{param['value']}\"")
def is_valid(self): valid = super().is_valid() if valid and self.instance.default and not self.edit_default: for field in self.Meta.fields: if field in ['tags', 'timeout']: continue if field in self.data: set_form_error( self, field, 'Not allowed to change on default scripts.') valid = False name = self.data.get('name') # none is used to tell the API to not run testing_scripts during # commissioning. if name is not None and name.lower() == 'none': set_form_error(self, 'name', '"none" is a reserved name.') valid = False # The name can't be a digit as MAAS allows scripts to be selected by # id. if name is not None and name.isdigit(): set_form_error(self, 'name', 'Cannot be a number.') valid = False if name is not None and pipes.quote(name) != name: set_form_error( self, 'name', 'Name contains disallowed characters, e.g. space or quotes.') valid = False # If comment and script exist __init__ combines both fields into a dict # to pass to VersionedTextFileField. if 'comment' in self.data: set_form_error( self, 'comment', '"comment" may only be used when specifying a "script" ' 'as well.') valid = False if 'script' in self.data: if not self._validate_results(self.instance.results): valid = False if 'parameters' in self.data: params_form = ParametersForm(data=self.data.get('parameters')) if not params_form.is_valid(): valid = False if (not valid and self.instance.script_id is not None and self.initial.get('script') != self.instance.script_id and self.instance.script.id is not None): # If form validation failed cleanup any new VersionedTextFile # created by the VersionedTextFileField. self.instance.script.delete() return valid
def clean_packages(self): if self.cleaned_data["packages"] == "": return self.instance.packages else: packages = json.loads(self.cleaned_data["packages"]) # Automatically convert into a list incase only one package is # needed. for key in ["apt", "snap", "url"]: if key in packages and not isinstance(packages[key], list): packages[key] = [packages[key]] for key in ["apt", "url"]: if key in packages: for package in packages[key]: if not isinstance(package, str): set_form_error( self, "packages", "Each %s package must be a string." % key, ) if "snap" in packages: for package in packages["snap"]: if isinstance(package, dict): if "name" not in package or not isinstance( package["name"], str): set_form_error( self, "packages", "Snap package name must be defined.", ) if "channel" in package and package["channel"] not in [ "stable", "edge", "beta", "candidate", ]: set_form_error( self, "packages", "Snap channel must be stable, edge, beta, " "or candidate.", ) if "mode" in package and package["mode"] not in [ "classic", "dev", "jail", ]: set_form_error( self, "packages", "Snap mode must be classic, dev, or jail.", ) elif not isinstance(package, str): set_form_error(self, "packages", "Snap package must be a string.") return packages
def clean(self): gateway_ip = self.cleaned_data.get('gateway_ip') source = self.cleaned_data.get('source') if gateway_ip: # This will not raise an AddrFormatErorr because it is validated at # the field first and if that fails the gateway_ip will be blank. if IPAddress(gateway_ip) not in source.get_ipnetwork(): set_form_error( self, 'gateway_ip', 'Enter an IP address in %s.' % source.cidr)
def clean_zone(self): value = self.cleaned_data[self.get_field_name('zone')] if value: nonexistent_names = detect_nonexistent_zone_names([value]) if len(nonexistent_names) > 0: error_msg = "No such zone: '%s'." % value set_form_error(self, self.get_field_name('zone'), error_msg) return None return value return None
def clean_not_in_pool(self): value = self.cleaned_data[self.get_field_name("not_in_pool")] if not value: return None nonexistent_names = detect_nonexistent_names(ResourcePool, value) if nonexistent_names: error_msg = "No such pool(s): %s." % ", ".join(nonexistent_names) set_form_error(self, self.get_field_name("not_in_pool"), error_msg) return None return value
def clean_zone(self): value = self.cleaned_data[self.get_field_name("zone")] if value: nonexistent_names = detect_nonexistent_names(Zone, [value]) if nonexistent_names: error_msg = "No such zone: '%s'." % value set_form_error(self, self.get_field_name("zone"), error_msg) return None return value return None
def clean_pool(self): value = self.cleaned_data[self.get_field_name("pool")] if value: nonexistent_names = detect_nonexistent_names(ResourcePool, [value]) if nonexistent_names: error_msg = "No such pool: '%s'." % value set_form_error(self, self.get_field_name("pool"), error_msg) return None return value return None
def clean_not_in_zone(self): value = self.cleaned_data[self.get_field_name('not_in_zone')] if not value: return None nonexistent_names = detect_nonexistent_names(Zone, value) if nonexistent_names: error_msg = "No such zone(s): %s." % ', '.join(nonexistent_names) set_form_error(self, self.get_field_name('not_in_zone'), error_msg) return None return value
def clean_not_in_zone(self): value = self.cleaned_data[self.get_field_name('not_in_zone')] if value is None or len(value) == 0: return None nonexistent_names = detect_nonexistent_zone_names(value) if len(nonexistent_names) > 0: error_msg = "No such zone(s): %s." % ', '.join(nonexistent_names) set_form_error(self, self.get_field_name('not_in_zone'), error_msg) return None return value
def clean(self): """Validate that the form is valid and that the destinations can accept the storage and/or interfaces configuration from the source.""" cleaned_data = super().clean() source = self.cleaned_data.get("source") if not source: # Django should be placing this automatically, but it does not # occur. So we force the setting of this error here. set_form_error(self, "source", "This field is required.") destinations = self.cleaned_data.get("destinations") destination_field = self.fields["destinations"] item_invalid = destination_field.error_messages["item_invalid"] storage = self.cleaned_data.get("storage", False) interfaces = self.cleaned_data.get("interfaces", False) if source and destinations: for index, dest in enumerate(destinations): if source == dest: error = prefix_validation_error( ValidationError( "Source machine cannot be a destination machine." ), prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) else: if storage: try: dest._get_storage_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) if interfaces: try: dest._get_interface_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) if not storage and not interfaces: set_form_error( self, "__all__", "Either storage or interfaces must be true." ) return cleaned_data
def _clean_boot_disk(self): """https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.esxi.install.doc/GUID-DEB8086A-306B-4239-BF76-E354679202FC.html ESXi 7.0 requires a boot disk of at least 32 GB of persistent storage such as HDD, SSD, or NVMe. Use USB, SD and non-USB flash media devices only for ESXi boot bank partitions. A boot device must not be shared between ESXi hosts. """ if self.boot_disk.size < (32 * 1024**3): set_form_error(self, "boot_size", "Boot disk must be at least 32Gb.")
def _validate_and_clean_url(self, param_name, param): """Validate and clean URL input.""" if param.get("allow_list", False): value = param["value"].split(",") else: value = [param["value"]] for url in value: # Django's validator requires a protocol but we accept just a # hostname or IP address. try: ip = ip_address(url) except ValueError: if "://" not in url: url = "http://%s" % url else: if ip.version == 4: url = "http://%s" % url else: url = "http://[%s]" % url # Allow all schemes supported by curl(used with the network # validation test) + icmp. try: validate_url( url, schemes=[ "icmp", "file", "ftp", "ftps", "gopher", "http", "https", "imap", "imaps", "ldap", "ldaps", "pop3", "pop3s", "rtmp", "rtsp", "scp", "sftp", "smb", "smbs", "smtp", "smtps", "telnet", "tftp", ], ) except ValidationError: set_form_error(self, param_name, "Invalid URL")
def _validate_and_clean_interface_id(self, param_name, param, result_params, ret): """Validate and clean interface input when id.""" interfaces = self._get_interfaces(id=int(param["value"])) if len(interfaces) != 1: set_form_error(self, param_name, "Interface id does not exist") else: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name]["value"] = self._interface_to_dict( interfaces[0]) ret.append(clean_param)
def clean(self): cleaned_data = super(PodForm, self).clean() if len(self.drivers) == 0: set_form_error( self, 'type', "No rack controllers are connected, unable to validate.") elif (not self.is_new and self.instance.power_type != self.cleaned_data.get('type')): set_form_error( self, 'type', "Cannot change the type of a pod. Delete and re-create the " "pod with a different type.") return cleaned_data
def _clean_mode_dhcp(self): # Can only have one DHCP link on an interface. dhcp_address = get_one( self.instance.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.DHCP)) if dhcp_address is not None: if dhcp_address.subnet is not None: set_form_error( self, "mode", "Interface is already set to DHCP from '%s'." % (dhcp_address.subnet)) else: set_form_error(self, "mode", "Interface is already set to DHCP.")
def _validate_and_clean_storage_id(self, param_name, param, result_params, ret): """Validate and clean storage input when id.""" try: bd = self._node.physicalblockdevice_set.get(id=int(param['value'])) except ObjectDoesNotExist: set_form_error(self, param_name, 'Physical block id does not exist') else: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name]['value'] = self._blockdevice_to_dict(bd) ret.append(clean_param)
def clean(self): # Circular imports. from maasserver.models.blockdevice import MIN_BLOCK_DEVICE_SIZE cleaned_data = super(BcacheStorageLayoutBase, self).clean() cache_device = self.get_cache_device() cache_size = self.get_cache_size() cache_no_part = self.get_cache_no_part() if cache_size is not None and cache_no_part: error_msg = ( "Cannot use cache_size and cache_no_part at the same time." ) set_form_error(self, "cache_size", error_msg) set_form_error(self, "cache_no_part", error_msg) elif cache_device is not None and cache_size is not None: if is_percentage(cache_size): cache_size = calculate_size_from_percentage( cache_device.size, cache_size ) if cache_size < MIN_BLOCK_DEVICE_SIZE: set_form_error( self, "cache_size", "Size is too small. Minimum size is %s." % MIN_BLOCK_DEVICE_SIZE, ) if cache_size > cache_device.size: set_form_error( self, "cache_size", "Size is too large. Maximum size is %s." % (cache_device.size), ) cleaned_data["cache_size"] = cache_size return cleaned_data
def _validate_and_clean_interface_id( self, param_name, param, result_params, ret): """Validate and clean interface input when id.""" try: interface = self._get_interfaces().get(id=int(param['value'])) except ObjectDoesNotExist: set_form_error( self, param_name, 'Interface id does not exist') else: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name]['value'] = self._interface_to_dict( interface) ret.append(clean_param)
def _clean_mode_link_up(self): # Cannot set LINK_UP unless no other IP address are attached to # this interface. But exclude STICKY addresses where the IP address is # null, because the user could be trying to change the subnet for a # LINK_UP address. And exclude DISCOVERED because what MAAS discovered # doesn't matter with regard to the user's intention. exclude_types = (IPADDRESS_TYPE.STICKY, IPADDRESS_TYPE.DISCOVERED) has_active_links = self.instance.ip_addresses.exclude( alloc_type__in=exclude_types, ip__isnull=True).count() > 0 if has_active_links and self.force is not True: set_form_error( self, "mode", "Cannot configure interface to link up (with no IP address) " "while other links are already configured. Specify force=True " "to override this behavior and delete all links.")
def _validate_and_clean_interface( self, param_name, param, result_params, ret ): """Validate and clean interface input.""" value = param["value"] for i in value.split(","): if ":" in i: # Allow users to specify an Interface using the vendor and # product. vendor, product = i.split(":") interfaces = self._get_interfaces( vendor=vendor, product=product ) if len(interfaces) != 0: for interface in interfaces: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name][ "value" ] = self._interface_to_dict(interface) ret.append(clean_param) continue if is_mac(i): interfaces = self._get_interfaces(mac_address=i) else: interfaces = self._get_interfaces( Q(name=i) | Q(vendor=i) | Q(product=i) | Q(tags__overlap=[i]) ) if len(interfaces) == 0: set_form_error( self, param_name, "Unknown interface for %s(%s)" % (self._node.fqdn, self._node.system_id), ) continue for interface in interfaces: clean_param = copy.deepcopy(result_params) clean_param[param_name] = copy.deepcopy(param) clean_param[param_name]["value"] = self._interface_to_dict( interface ) ret.append(clean_param)