def test_creates_new_link(self): old_ver = VersionedTextFile.objects.create(data=factory.make_string()) versioned_text_file_field = VersionedTextFileField(initial=old_ver) data = factory.make_string() new_ver = versioned_text_file_field.clean(data) self.assertEquals(data, new_ver.data) self.assertEquals(old_ver, new_ver.previous_version)
def test_create_new_accepts_dict_with_comment(self): data = factory.make_string() comment = factory.make_name('comment') versioned_text_file_field = VersionedTextFileField() versioned_text_file = versioned_text_file_field.clean( {'data': data, 'comment': comment}) self.assertEquals(data, versioned_text_file.data) self.assertEquals(comment, versioned_text_file.comment) self.assertIsNone(versioned_text_file.previous_version)
def test_creates_new_link_accepts_dict(self): old_ver = VersionedTextFile.objects.create(data=factory.make_string()) versioned_text_file_field = VersionedTextFileField(initial=old_ver) data = factory.make_string() comment = factory.make_name('comment') new_ver = versioned_text_file_field.clean( {'new_data': data, 'comment': comment}) self.assertEquals(data, new_ver.data) self.assertEquals(comment, new_ver.comment) self.assertEquals(old_ver, new_ver.previous_version)
class ScriptForm(ModelForm): script_type = CharField( label="Script type", required=False, help_text="Script type", initial=str(SCRIPT_TYPE.TESTING), ) hardware_type = CharField( label="Hardware type", required=False, help_text="The hardware type the script configures or tests.", initial=str(HARDWARE_TYPE.NODE), ) parallel = CharField( label="Parallel", required=False, help_text="Whether the script may run in parallel with other scripts.", initial=str(SCRIPT_PARALLEL.DISABLED), ) packages = CharField( label="Packages", required=False, help_text="Packages to be installed with script.", initial="", ) timeout = DurationField( label="Timeout", required=False, help_text="Timeout", initial=timedelta(0), ) script = VersionedTextFileField(label="Script", help_text="Script content") comment = CharField( label="Comment", required=False, help_text="Description of change", initial="", ) for_hardware = CharField( label="For hardware", required=False, help_text="Hardware identifiers this script requires to run.", initial="", ) apply_configured_networking = BooleanField(required=False) class Meta: model = Script fields = ( "name", "title", "description", "tags", "script_type", "hardware_type", "parallel", "packages", "timeout", "destructive", "script", "for_hardware", "may_reboot", "recommission", "apply_configured_networking", ) def __init__(self, instance=None, data=None, edit_default=False, **kwargs): self.edit_default = edit_default if instance is None: script_data_key = "data" else: script_data_key = "new_data" data = data.copy() if "comment" in data and "script" in data: script_data = { "comment": data.get("comment"), script_data_key: data.get("script"), } data["script"] = script_data data.pop("comment") # Alias type to script_type to allow for consistent naming in the API. if "type" in data and "script_type" not in data: data["script_type"] = data["type"] # self.data is a QueryDict. pop returns a list containing the value # while directly accessing it returns just the value. data.pop("type") super().__init__(instance=instance, data=data, **kwargs) if instance is None: for field in ["name", "script"]: self.fields[field].required = True else: for field in ["name", "script"]: self.fields[field].required = False self.fields["script"].initial = instance.script # Reading the embedded YAML must happen at the end of initialization # so the fields set are validated. if "script" in self.data: self._read_script() 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 _clean_script(self, parsed_yaml): """Clean script data and validate input.""" # Tags and timeout may not be updated from new embedded YAML. This # allows users to receive updated scripts from an upstream maintainer, # such as Canonical, while maintaining user defined tags and timeout. # Tags must be a comma seperated string for the form. tags = parsed_yaml.pop("tags", None) if (tags is not None and self.instance.id is None and "tags" not in self.data): tags_valid = True if isinstance(tags, str): self.data["tags"] = tags elif isinstance(tags, list): for tag in tags: if not isinstance(tag, str): tags_valid = False continue if tags_valid: self.data["tags"] = ",".join(tags) else: tags_valid = False if not tags_valid: set_form_error( self, "tags", "Embedded tags must be a string of comma seperated " "values, or a list of strings.", ) # Timeout must be a string for the form. timeout = parsed_yaml.pop("timeout", None) if (timeout is not None and self.instance.id is None and "timeout" not in self.data): self.data["timeout"] = str(timeout) # Packages and for_hardware must be a JSON string for the form. for key in ["packages", "for_hardware"]: value = parsed_yaml.pop(key, None) if value is not None and key not in self.data: self.data[key] = json.dumps(value) for key, value in parsed_yaml.items(): if key in self.fields: error = False if key not in self.data: self.data[key] = value elif key == "script_type": # The deprecated Commissioning API always sets the # script_type to commissioning as it has always only # accepted commissioning scripts while the form sets # the default type to testing. If the YAML matches the # type allow it. try: if translate_script_type( value) != translate_script_type( self.data[key]): error = True except ValidationError: error = True elif value != self.data[key]: # Only allow form data for fields defined in the YAML if # the data matches. error = True if error: set_form_error( self, key, "May not override values defined in embedded YAML.", ) def _read_script(self): """Read embedded YAML configuration in a script. Search for supported MAAS script metadata in the script and read the values. Leading '#' are ignored. If the values are fields they will be entered in the form. """ yaml_delim = re.compile( r"\s*#\s*-+\s*(Start|End) MAAS (?P<version>\d+\.\d+) " r"script metadata\s+-+", re.I, ) found_version = None yaml_content = "" if isinstance(self.data["script"], dict): if "new_data" in self.data["script"]: script = self.data["script"]["new_data"] else: script = self.data["script"]["data"] else: script = self.data["script"] script_splitlines = script.splitlines() if len(script_splitlines) >= 1 and not script_splitlines[0].startswith( "#!/"): set_form_error(self, "script", "Must start with shebang.") for line in script_splitlines[1:]: m = yaml_delim.search(line) if m is not None: if found_version is None and m.group("version") == "1.0": # Found the start of the embedded YAML found_version = m.group("version") continue elif found_version == m.group("version"): # Found the end of the embedded YAML break elif found_version is not None and line.strip() != "": # Capture all lines inbetween the deliminator if "#" not in line: set_form_error(self, "script", 'Missing "#" on YAML line.') return yaml_content += "%s\n" % line.split("#", 1)[1] try: parsed_yaml = yaml.safe_load(yaml_content) except yaml.YAMLError as err: set_form_error(self, "script", "Invalid YAML: %s" % err) return if not isinstance(parsed_yaml, dict): return self.instance.results = parsed_yaml.pop("results", {}) self.instance.parameters = parsed_yaml.pop("parameters", {}) self._clean_script(parsed_yaml) 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_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 clean(self): cleaned_data = super().clean() # If a field wasn't passed in keep the old values when updating. if self.instance.id is not None: for field in self._meta.fields: if field not in self.data: cleaned_data[field] = getattr(self.instance, field) script_type = cleaned_data["script_type"] if script_type == "": cleaned_data["script_type"] = self.instance.script_type else: try: cleaned_data["script_type"] = translate_script_type( script_type) except ValidationError as e: set_form_error(self, "script_type", e) hardware_type = cleaned_data["hardware_type"] if hardware_type == "": cleaned_data["hardware_type"] = self.instance.hardware_type else: try: cleaned_data["hardware_type"] = translate_hardware_type( hardware_type) except ValidationError as e: set_form_error(self, "hardware_type", e) parallel = cleaned_data["parallel"] if parallel == "": cleaned_data["parallel"] = self.instance.parallel else: try: cleaned_data["parallel"] = translate_script_parallel(parallel) except ValidationError as e: set_form_error(self, "parallel", e) return cleaned_data 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 save(self, *args, **kwargs): request = kwargs.pop("request", None) endpoint = kwargs.pop("endpoint", None) script = super(ScriptForm, self).save(*args, **kwargs) # Create audit event log if endpoint and request supplied. if request is not None and endpoint is not None: create_audit_event( EVENT_TYPES.SETTINGS, endpoint, request, None, description="Saved script '%s'." % script.name, ) return script
class DHCPSnippetForm(MAASModelForm): """DHCP snippet creation/edition form.""" name = forms.CharField(label="Name", required=False, help_text="The name of the DHCP snippet.") value = VersionedTextFileField(label="DHCP Snippet", required=False, help_text="The DHCP Snippet") description = forms.CharField( label="Description", required=False, help_text="The description of what the DHCP snippet does.", ) enabled = forms.BooleanField( label="Enabled", required=False, help_text="Whether or not the DHCP snippet is enabled.", ) node = NodeChoiceField( label="Node", queryset=Node.objects.all(), required=False, initial=None, help_text="The node which the DHCP snippet is for.", ) subnet = SpecifierOrModelChoiceField( label="Subnet", queryset=Subnet.objects.all(), required=False, help_text="The subnet which the DHCP snippet is for.", ) global_snippet = forms.BooleanField( label="Global DHCP Snippet", required=False, help_text=( "Set the DHCP snippet to be global, removes links to nodes or " "subnets"), ) class Meta: model = DHCPSnippet fields = ( "name", "value", "description", "enabled", "node", "subnet", "global_snippet", ) def __init__(self, instance=None, request=None, **kwargs): super().__init__(instance=instance, **kwargs) if instance is None: for field in ["name", "value"]: self.fields[field].required = True self.initial["enabled"] = True else: self.fields["value"].initial = self.instance.value if instance is not None and instance.node is not None: self.initial["node"] = self.instance.node.system_id def clean(self): cleaned_data = super().clean() if cleaned_data.get("global_snippet", False): cleaned_data["node"] = None self.instance.node = None cleaned_data["subnet"] = None self.instance.subnet = None elif (self.instance.subnet == cleaned_data.get("subnet") and cleaned_data.get("node") is not None): cleaned_data["subnet"] = None self.instance.subnet = None elif (self.instance.node == cleaned_data.get("node") and cleaned_data.get("subnet") is not None): cleaned_data["node"] = None self.instance.node = None return cleaned_data def is_valid(self): valid = super().is_valid() if valid: # Often the first error can cause cascading errors. Showing all of # these errors can be confusing so only show the first if there is # one. first_error = None for error in validate_dhcp_config(self.instance): valid = False if first_error is None: first_error = error else: if error["line_num"] < first_error["line_num"]: first_error = error if first_error is not None: set_form_error(self, "value", first_error["error"]) # If the DHCPSnippet isn't valid cleanup the value if not valid and self.initial.get("value") != self.instance.value_id: self.instance.value.delete() return valid def save(self, endpoint, request): dhcp_snippet = super(DHCPSnippetForm, self).save() create_audit_event( EVENT_TYPES.SETTINGS, endpoint, request, None, description=("%s DHCP snippet '%s'." % ( "Updated" if self.is_update else "Created", dhcp_snippet.name, )), ) return dhcp_snippet
class ScriptForm(ModelForm): script_type = CharField(label='Script type', required=False, help_text='Script type', initial=str(SCRIPT_TYPE.TESTING)) hardware_type = CharField( label='Hardware type', required=False, help_text='The hardware type the script configures or tests.', initial=str(HARDWARE_TYPE.NODE)) parallel = CharField( label='Parallel', required=False, help_text='Whether the script may run in parallel with other scripts.', initial=str(SCRIPT_PARALLEL.DISABLED)) packages = CharField(label='Packages', required=False, help_text='Packages to be installed with script.', initial='') timeout = DurationField(label='Timeout', required=False, help_text='Timeout', initial=timedelta(0)) script = VersionedTextFileField(label='Script', help_text='Script content') comment = CharField(label='Comment', required=False, help_text='Description of change', initial='') for_hardware = CharField( label='For hardware', required=False, help_text='Hardware identifiers this script requires to run.', initial='') class Meta: model = Script fields = ( 'name', 'title', 'description', 'tags', 'script_type', 'hardware_type', 'parallel', 'packages', 'timeout', 'destructive', 'script', 'for_hardware', 'may_reboot', 'recommission', ) def __init__(self, instance=None, data=None, edit_default=False, **kwargs): self.edit_default = edit_default if instance is None: script_data_key = 'data' else: script_data_key = 'new_data' data = data.copy() if 'comment' in data and 'script' in data: script_data = { 'comment': data.get('comment'), script_data_key: data.get('script'), } data['script'] = script_data data.pop('comment') # Alias type to script_type to allow for consistent naming in the API. if 'type' in data and 'script_type' not in data: data['script_type'] = data['type'] # self.data is a QueryDict. pop returns a list containing the value # while directly accessing it returns just the value. data.pop('type') super().__init__(instance=instance, data=data, **kwargs) if instance is None: for field in ['name', 'script']: self.fields[field].required = True else: for field in ['name', 'script']: self.fields[field].required = False self.fields['script'].initial = instance.script # Reading the embedded YAML must happen at the end of initialization # so the fields set are validated. if 'script' in self.data: self._read_script() 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 _read_script(self): """Read embedded YAML configuration in a script. Search for supported MAAS script metadata in the script and read the values. Leading '#' are ignored. If the values are fields they will be entered in the form. """ yaml_delim = re.compile( '\s*#\s*-+\s*(Start|End) MAAS (?P<version>\d+\.\d+) ' 'script metadata\s+-+', re.I) found_version = None yaml_content = '' if isinstance(self.data['script'], dict): if 'new_data' in self.data['script']: script = self.data['script']['new_data'] else: script = self.data['script']['data'] else: script = self.data['script'] script_splitlines = script.splitlines() if (len(script_splitlines) >= 1 and not script_splitlines[0].startswith('#!/')): set_form_error(self, 'script', 'Must start with shebang.') for line in script_splitlines[1:]: m = yaml_delim.search(line) if m is not None: if found_version is None and m.group('version') == '1.0': # Found the start of the embedded YAML found_version = m.group('version') continue elif found_version == m.group('version'): # Found the end of the embedded YAML break elif found_version is not None and line.strip() != '': # Capture all lines inbetween the deliminator if '#' not in line: set_form_error(self, 'script', 'Missing "#" on YAML line.') return yaml_content += '%s\n' % line.split('#', 1)[1] try: parsed_yaml = yaml.safe_load(yaml_content) except yaml.YAMLError as err: set_form_error(self, 'script', 'Invalid YAML: %s' % err) return if not isinstance(parsed_yaml, dict): return self.instance.results = parsed_yaml.pop('results', {}) self.instance.parameters = parsed_yaml.pop('parameters', {}) # Tags and timeout may not be updated from new embedded YAML. This # allows users to receive updated scripts from an upstream maintainer, # such as Canonical, while maintaining user defined tags and timeout. # Tags must be a comma seperated string for the form. tags = parsed_yaml.pop('tags', None) if (tags is not None and self.instance.id is None and 'tags' not in self.data): tags_valid = True if isinstance(tags, str): self.data['tags'] = tags elif isinstance(tags, list): for tag in tags: if not isinstance(tag, str): tags_valid = False continue if tags_valid: self.data['tags'] = ','.join(tags) else: tags_valid = False if not tags_valid: set_form_error( self, 'tags', 'Embedded tags must be a string of comma seperated ' 'values, or a list of strings.') # Timeout must be a string for the form. timeout = parsed_yaml.pop('timeout', None) if (timeout is not None and self.instance.id is None and 'timeout' not in self.data): self.data['timeout'] = str(timeout) # Packages and for_hardware must be a JSON string for the form. for key in ['packages', 'for_hardware']: value = parsed_yaml.pop(key, None) if value is not None and key not in self.data: self.data[key] = json.dumps(value) for key, value in parsed_yaml.items(): if key in self.fields: if key not in self.data: self.data[key] = value else: set_form_error( self, key, 'May not override values defined in embedded YAML.') 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_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( '^modalias:.+|pci:[\da-f]{4}:[\da-f]{4}|' 'usb:[\da-f]{4}:[\da-f]{4}|' 'system_vendor:.*|' 'system_product:.*|' 'system_version:.*|' 'mainboard_vendor:.*|' '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 clean(self): cleaned_data = super().clean() # If a field wasn't passed in keep the old values when updating. if self.instance.id is not None: for field in self._meta.fields: if field not in self.data: cleaned_data[field] = getattr(self.instance, field) script_type = cleaned_data['script_type'] if script_type == '': cleaned_data['script_type'] = self.instance.script_type else: try: cleaned_data['script_type'] = translate_script_type( script_type) except ValidationError as e: set_form_error(self, 'script_type', e) hardware_type = cleaned_data['hardware_type'] if hardware_type == '': cleaned_data['hardware_type'] = self.instance.hardware_type else: try: cleaned_data['hardware_type'] = translate_hardware_type( hardware_type) except ValidationError as e: set_form_error(self, 'hardware_type', e) parallel = cleaned_data['parallel'] if parallel == '': cleaned_data['parallel'] = self.instance.parallel else: try: cleaned_data['parallel'] = translate_script_parallel(parallel) except ValidationError as e: set_form_error(self, 'parallel', e) return cleaned_data 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 save(self, *args, **kwargs): request = kwargs.pop('request', None) endpoint = kwargs.pop('endpoint', None) script = super(ScriptForm, self).save(*args, **kwargs) # Create audit event log if endpoint and request supplied. if request is not None and endpoint is not None: create_audit_event(EVENT_TYPES.SETTINGS, endpoint, request, None, description=("Script %s" % script.name + " saved for '%(username)s'.")) return script
def test_update_does_nothing_on_none(self): data = VersionedTextFile.objects.create(data=factory.make_string()) versioned_text_file_field = VersionedTextFileField(initial=data) self.assertEquals(data, versioned_text_file_field.clean(None))
def test_create_requires_value(self): versioned_text_file_field = VersionedTextFileField() self.assertRaises(ValidationError, versioned_text_file_field.clean, None)
def test_creates_new(self): data = factory.make_string() versioned_text_file_field = VersionedTextFileField() versioned_text_file = versioned_text_file_field.clean(data) self.assertEquals(data, versioned_text_file.data) self.assertIsNone(versioned_text_file.previous_version)
class DHCPSnippetForm(MAASModelForm): """DHCP snippet creation/edition form.""" name = forms.CharField(label="Name", required=False, help_text=("The name of the DHCP snippet.")) value = VersionedTextFileField(label="DHCP Snippet", required=False, help_text="The DHCP Snippet") description = forms.CharField( label="Description", required=False, help_text=("The description of what the DHCP snippet does.")) enabled = forms.BooleanField( label="Enabled", required=False, help_text=("Whether or not the DHCP snippet is enabled.")) node = NodeChoiceField( label="Node", queryset=Node.objects.all(), required=False, initial=None, help_text=("The node which the DHCP snippet is for.")) subnet = SpecifierOrModelChoiceField( label="Subnet", queryset=Subnet.objects.all(), required=False, help_text="The subnet which the DHCP snippet is for.") global_snippet = forms.BooleanField( label="Global DHCP Snippet", required=False, help_text=( "Set the DHCP snippet to be global, removes links to nodes or " "subnets")) class Meta: model = DHCPSnippet fields = ( 'name', 'value', 'description', 'enabled', 'node', 'subnet', 'global_snippet', ) def __init__(self, instance=None, request=None, **kwargs): super().__init__(instance=instance, **kwargs) if instance is None: for field in ['name', 'value']: self.fields[field].required = True self.initial['enabled'] = True else: self.fields['value'].initial = self.instance.value if instance is not None and instance.node is not None: self.initial['node'] = self.instance.node.system_id def clean(self): cleaned_data = super().clean() if cleaned_data.get('global_snippet', False): cleaned_data['node'] = None self.instance.node = None cleaned_data['subnet'] = None self.instance.subnet = None elif (self.instance.subnet == cleaned_data.get('subnet') and cleaned_data.get('node') is not None): cleaned_data['subnet'] = None self.instance.subnet = None elif (self.instance.node == cleaned_data.get('node') and cleaned_data.get('subnet') is not None): cleaned_data['node'] = None self.instance.node = None return cleaned_data def is_valid(self): valid = super().is_valid() if valid: # Often the first error can cause cascading errors. Showing all of # these errors can be confusing so only show the first if there is # one. first_error = None for error in validate_dhcp_config(self.instance): valid = False if first_error is None: first_error = error else: if error['line_num'] < first_error['line_num']: first_error = error if first_error is not None: set_form_error(self, 'value', first_error['error']) # If the DHCPSnippet isn't valid cleanup the value if not valid and self.initial.get('value') != self.instance.value_id: self.instance.value.delete() return valid