Exemple #1
0
 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)
Exemple #2
0
 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)
Exemple #3
0
 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)
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
 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))
Exemple #8
0
 def test_create_requires_value(self):
     versioned_text_file_field = VersionedTextFileField()
     self.assertRaises(ValidationError, versioned_text_file_field.clean,
                       None)
Exemple #9
0
 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)
Exemple #10
0
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