Beispiel #1
0
class NodeCommissionResult(CleanSave, TimestampedModel):
    """Storage for data returned from node commissioning.

    Commissioning a node results in various bits of data that need to be
    stored, such as lshw output.  This model allows storing of this data
    as unicode text, with an arbitrary name, for later retrieval.

    :ivar node: The context :class:`Node`.
    :ivar status: If this data results from the execution of a script, this
        is the status of this execution.  This can be "OK", "FAILED" or
        "WORKING" for progress reports.
    :ivar name: A unique name to use for the data being stored.
    :ivar data: The file's actual data, unicode only.
    """
    class Meta(DefaultMeta):
        unique_together = ('node', 'name')

    objects = NodeCommissionResultManager()

    node = ForeignKey('maasserver.Node',
                      null=False,
                      editable=False,
                      unique=False)
    script_result = IntegerField(editable=False)
    name = CharField(max_length=255, unique=False, editable=False)
    data = BinaryField(max_length=1024 * 1024,
                       editable=True,
                       blank=True,
                       default=b'',
                       null=False)
Beispiel #2
0
class CommissioningScript(Model):
    """User-provided commissioning script.

    Actually a commissioning "script" could be a binary, e.g. because a
    hardware vendor supplied an update in the form of a binary executable.
    """
    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""

    objects = CommissioningScriptManager()

    name = CharField(max_length=255, null=False, editable=True, unique=True)
    content = BinaryField(null=False)
Beispiel #3
0
class FileStorage(CleanSave, Model):
    """A simple file storage keyed on file name.

    :ivar filename: A file name to use for the data being stored.
    :ivar owner: This file's owner..
    :ivar content: The file's actual data.
    """
    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""

        unique_together = ("filename", "owner")

    filename = CharField(max_length=255, unique=False, editable=False)
    content = BinaryField(null=False, blank=True)
    # owner can be None: this is to support upgrading existing
    # installations where the files were not linked to users yet.
    owner = ForeignKey(
        User,
        default=None,
        blank=True,
        null=True,
        editable=False,
        on_delete=PROTECT,
    )
    key = CharField(
        max_length=36,
        unique=True,
        default=generate_filestorage_key,
        editable=False,
    )

    objects = FileStorageManager()

    def __str__(self):
        return self.filename

    @property
    def anon_resource_uri(self):
        """URI where the content of the file can be retrieved anonymously."""
        params = {"op": "get_by_key", "key": self.key}
        url = "%s?%s" % (reverse("files_handler"), urlencode(params))
        return url
Beispiel #4
0
class NodeUserData(CleanSave, Model):
    """User-data portion of a node's metadata.

    When cloud-init sets up a node, it retrieves specific data for that node
    from the metadata service.  One portion of that is the "user-data" binary
    blob.

    :ivar node: Node that this is for.
    :ivar data: base64-encoded data.
    """
    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""

    objects = NodeUserDataManager()

    node = OneToOneField("maasserver.Node",
                         null=False,
                         editable=False,
                         on_delete=CASCADE)
    data = BinaryField(null=False)
Beispiel #5
0
class BinaryFieldModel(Model):
    """Test model for BinaryField.  Contains nothing but a BinaryField."""

    data = BinaryField(null=True)
Beispiel #6
0
class ScriptResult(CleanSave, TimestampedModel):

    # Force model into the metadataserver namespace.
    class Meta(DefaultMeta):
        pass

    script_set = ForeignKey(ScriptSet, editable=False, on_delete=CASCADE)

    # All ScriptResults except commissioning scripts will be linked to a Script
    # as commissioning scripts are still embedded in the MAAS source.
    script = ForeignKey(Script,
                        editable=False,
                        blank=True,
                        null=True,
                        on_delete=CASCADE)

    # Any parameters set by MAAS or the user which should be passed to the
    # running script.
    parameters = JSONObjectField(blank=True, default={})

    # If the result is in reference to a particular block device link it.
    physical_blockdevice = ForeignKey(PhysicalBlockDevice,
                                      editable=False,
                                      blank=True,
                                      null=True,
                                      on_delete=CASCADE)

    script_version = ForeignKey(VersionedTextFile,
                                blank=True,
                                null=True,
                                editable=False,
                                on_delete=SET_NULL)

    status = IntegerField(choices=SCRIPT_STATUS_CHOICES,
                          default=SCRIPT_STATUS.PENDING)

    exit_status = IntegerField(blank=True, null=True)

    # Used by the builtin commissioning scripts and installation result. Also
    # stores the Script name incase the Script is deleted but the result isn't.
    script_name = CharField(max_length=255,
                            unique=False,
                            editable=False,
                            null=True)

    output = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    stdout = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    stderr = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    result = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    # When the script started to run
    started = DateTimeField(editable=False, null=True, blank=True)

    # When the script finished running
    ended = DateTimeField(editable=False, null=True, blank=True)

    @property
    def name(self):
        if self.script is not None:
            return self.script.name
        elif self.script_name is not None:
            return self.script_name
        else:
            return "Unknown"

    @property
    def status_name(self):
        return SCRIPT_STATUS_CHOICES[self.status][1]

    @property
    def runtime(self):
        if None not in (self.ended, self.started):
            runtime = self.ended - self.started
            return str(runtime - timedelta(microseconds=runtime.microseconds))
        else:
            return ''

    @property
    def starttime(self):
        if self.started is not None:
            return self.started.timestamp()
        else:
            return ''

    @property
    def endtime(self):
        if self.ended is not None:
            return self.ended.timestamp()
        else:
            return ''

    @property
    def estimated_runtime(self):
        # If there is a runtime the script has completed, no need to calculate
        # an estimate.
        if self.runtime != '':
            return self.runtime
        runtime = None
        # Get an estimated runtime from previous runs.
        for script_result in self.history:
            # Only look at passed results when calculating an estimated
            # runtime. Failed results may take longer or shorter than
            # average. Don't use self.history.filter for this as the now
            # cached history list may be used elsewhere.
            if script_result.status != SCRIPT_STATUS.PASSED:
                continue
            # LP: #1730799 - Old results may not have started set.
            if script_result.started is None:
                script_result.started = script_result.ended
                script_result.save(update_fields=['started'])
            previous_runtime = script_result.ended - script_result.started
            if runtime is None:
                runtime = previous_runtime
            else:
                runtime += previous_runtime
                runtime = runtime / 2
        if runtime is None:
            if self.script is not None and self.script.timeout != timedelta(0):
                # If there were no previous runs use the script's timeout.
                return str(self.script.timeout - timedelta(
                    microseconds=self.script.timeout.microseconds))
            else:
                return 'Unknown'
        else:
            return str(runtime - timedelta(microseconds=runtime.microseconds))

    def __str__(self):
        return "%s/%s" % (self.script_set.node.system_id, self.name)

    def read_results(self):
        """Read the results YAML file and validate it."""
        try:
            parsed_yaml = yaml.safe_load(self.result)
        except yaml.YAMLError as err:
            raise ValidationError(err)

        if parsed_yaml is None:
            # No results were given.
            return {}
        elif not isinstance(parsed_yaml, dict):
            raise ValidationError('YAML must be a dictionary.')

        if parsed_yaml.get('status') not in [
                'passed', 'failed', 'degraded', 'timedout', None
        ]:
            raise ValidationError(
                'status must be "passed", "failed", "degraded", or '
                '"timedout".')

        results = parsed_yaml.get('results')
        if results is None:
            # Results are not defined.
            return parsed_yaml
        elif isinstance(results, dict):
            for key, value in results.items():
                if not isinstance(key, str):
                    raise ValidationError(
                        'All keys in the results dictionary must be strings.')

                if not isinstance(value, list):
                    value = [value]
                for i in value:
                    if type(i) not in [str, float, int, bool]:
                        raise ValidationError(
                            'All values in the results dictionary must be '
                            'a string, float, int, or bool.')
        else:
            raise ValidationError('results must be a dictionary.')

        return parsed_yaml

    def store_result(self,
                     exit_status=None,
                     output=None,
                     stdout=None,
                     stderr=None,
                     result=None,
                     script_version_id=None,
                     timedout=False):
        # Don't allow ScriptResults to be overwritten unless the node is a
        # controller. Controllers are allowed to overwrite their results to
        # prevent new ScriptSets being created everytime a controller starts.
        # This also allows us to avoid creating an RPC call for the rack
        # controller to create a new ScriptSet.
        if not self.script_set.node.is_controller:
            # Allow PENDING, INSTALLING, and RUNNING scripts incase the node
            # didn't inform MAAS the Script was being run, it just uploaded
            # results.
            assert self.status in (SCRIPT_STATUS.PENDING,
                                   SCRIPT_STATUS.INSTALLING,
                                   SCRIPT_STATUS.RUNNING)
            assert self.output == b''
            assert self.stdout == b''
            assert self.stderr == b''
            assert self.result == b''
            assert self.script_version is None

        if timedout:
            self.status = SCRIPT_STATUS.TIMEDOUT
        elif exit_status is not None:
            self.exit_status = exit_status
            if exit_status == 0:
                self.status = SCRIPT_STATUS.PASSED
            elif self.status == SCRIPT_STATUS.INSTALLING:
                self.status = SCRIPT_STATUS.FAILED_INSTALLING
            else:
                self.status = SCRIPT_STATUS.FAILED

        if output is not None:
            self.output = Bin(output)
        if stdout is not None:
            self.stdout = Bin(stdout)
        if stderr is not None:
            self.stderr = Bin(stderr)
        if result is not None:
            self.result = Bin(result)
            try:
                parsed_yaml = self.read_results()
            except ValidationError as err:
                err_msg = (
                    "%s(%s) sent a script result with invalid YAML: %s" %
                    (self.script_set.node.fqdn, self.script_set.node.system_id,
                     err.message))
                logger.error(err_msg)
                Event.objects.create_node_event(
                    system_id=self.script_set.node.system_id,
                    event_type=EVENT_TYPES.SCRIPT_RESULT_ERROR,
                    event_description=err_msg)
            else:
                status = parsed_yaml.get('status')
                if status == 'passed':
                    self.status = SCRIPT_STATUS.PASSED
                elif status == 'failed':
                    self.status = SCRIPT_STATUS.FAILED
                elif status == 'degraded':
                    self.status = SCRIPT_STATUS.DEGRADED
                elif status == 'timedout':
                    self.status = SCRIPT_STATUS.TIMEDOUT

        if self.script:
            if script_version_id is not None:
                for script in self.script.script.previous_versions():
                    if script.id == script_version_id:
                        self.script_version = script
                        break
                if self.script_version is None:
                    err_msg = (
                        "%s(%s) sent a script result for %s(%d) with an "
                        "unknown script version(%d)." %
                        (self.script_set.node.fqdn,
                         self.script_set.node.system_id, self.script.name,
                         self.script.id, script_version_id))
                    logger.error(err_msg)
                    Event.objects.create_node_event(
                        system_id=self.script_set.node.system_id,
                        event_type=EVENT_TYPES.SCRIPT_RESULT_ERROR,
                        event_description=err_msg)
            else:
                # If no script version was given assume the latest version
                # was run.
                self.script_version = self.script.script

        # If commissioning result check if its a builtin script, if so run its
        # hook before committing to the database.
        if (self.script_set.result_type == RESULT_TYPE.COMMISSIONING
                and self.name in NODE_INFO_SCRIPTS):
            post_process_hook = NODE_INFO_SCRIPTS[self.name]['hook']
            err = ("%s(%s): commissioning script '%s' failed during "
                   "post-processing." %
                   (self.script_set.node.fqdn, self.script_set.node.system_id,
                    self.name))
            # Circular imports.
            from metadataserver.api import try_or_log_event
            try_or_log_event(self.script_set.node,
                             None,
                             err,
                             post_process_hook,
                             node=self.script_set.node,
                             output=self.stdout,
                             exit_status=self.exit_status)
        self.save()

    @property
    def history(self):
        qs = ScriptResult.objects.filter(
            script_set__node_id=self.script_set.node_id)
        if self.script is not None:
            qs = qs.filter(script=self.script)
        else:
            qs = qs.filter(script_name=self.script_name)
        # XXX ltrager 2017-10-05 - Shows script runs from before MAAS supported
        # the hardware type or physical_blockdevice fields in history.
        # Solves LP: #1721524
        qs = qs.filter(
            Q(physical_blockdevice=self.physical_blockdevice)
            | Q(physical_blockdevice__isnull=True))
        qs = qs.order_by('-id')
        return qs

    def save(self, *args, **kwargs):
        if self.started is None and self.status == SCRIPT_STATUS.RUNNING:
            self.started = datetime.now()
            if 'update_fields' in kwargs:
                kwargs['update_fields'].append('started')
        elif self.ended is None and self.status in {
                SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
                SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED,
                SCRIPT_STATUS.DEGRADED, SCRIPT_STATUS.FAILED_INSTALLING
        }:
            self.ended = datetime.now()
            if 'update_fields' in kwargs:
                kwargs['update_fields'].append('ended')
            # LP: #1730799 - If a script is run quickly the POST telling MAAS
            # the script has started comes in after the POST telling MAAS the
            # result.
            if self.started is None:
                self.started = self.ended
                if 'update_fields' in kwargs:
                    kwargs['update_fields'].append('started')

        if self.id is None and self.physical_blockdevice is None:
            for param in self.parameters.values():
                if ('value' in param and isinstance(param['value'], dict)
                        and 'physical_blockdevice' in param['value']):
                    physical_blockdevice = param['value'].pop(
                        'physical_blockdevice')
                    self.physical_blockdevice = physical_blockdevice
                    param['value'][
                        'physical_blockdevice_id'] = physical_blockdevice.id

        return super().save(*args, **kwargs)
Beispiel #7
0
class ScriptResult(CleanSave, TimestampedModel):

    # Force model into the metadataserver namespace.
    class Meta(DefaultMeta):
        pass

    script_set = ForeignKey(ScriptSet, editable=False, on_delete=CASCADE)

    # All ScriptResults except commissioning scripts will be linked to a Script
    # as commissioning scripts are still embedded in the MAAS source.
    script = ForeignKey(Script,
                        editable=False,
                        blank=True,
                        null=True,
                        on_delete=CASCADE)

    # Any parameters set by MAAS or the user which should be passed to the
    # running script.
    parameters = JSONObjectField(blank=True, default={})

    # If the result is in reference to a particular block device link it.
    physical_blockdevice = ForeignKey(
        PhysicalBlockDevice,
        editable=False,
        blank=True,
        null=True,
        on_delete=CASCADE,
    )

    # If the result is in reference to a particular Interface link it.
    interface = ForeignKey(Interface,
                           editable=False,
                           blank=True,
                           null=True,
                           on_delete=CASCADE)

    script_version = ForeignKey(
        VersionedTextFile,
        blank=True,
        null=True,
        editable=False,
        on_delete=SET_NULL,
    )

    status = IntegerField(choices=SCRIPT_STATUS_CHOICES,
                          default=SCRIPT_STATUS.PENDING)

    exit_status = IntegerField(blank=True, null=True)

    # Used by the builtin commissioning scripts and installation result. Also
    # stores the Script name incase the Script is deleted but the result isn't.
    script_name = CharField(max_length=255,
                            unique=False,
                            editable=False,
                            null=True)

    output = BinaryField(max_length=1024 * 1024, blank=True, default=b"")

    stdout = BinaryField(max_length=1024 * 1024, blank=True, default=b"")

    stderr = BinaryField(max_length=1024 * 1024, blank=True, default=b"")

    result = BinaryField(max_length=1024 * 1024, blank=True, default=b"")

    # When the script started to run
    started = DateTimeField(editable=False, null=True, blank=True)

    # When the script finished running
    ended = DateTimeField(editable=False, null=True, blank=True)

    # Whether or not the failed script result should be suppressed.
    suppressed = BooleanField(default=False)

    @property
    def name(self):
        if self.script is not None:
            return self.script.name
        elif self.script_name is not None:
            return self.script_name
        else:
            return "Unknown"

    @property
    def status_name(self):
        return SCRIPT_STATUS_CHOICES[self.status][1]

    @property
    def runtime(self):
        if None not in (self.ended, self.started):
            runtime = self.ended - self.started
            return str(runtime - timedelta(microseconds=runtime.microseconds))
        else:
            return ""

    @property
    def starttime(self):
        if self.started is not None:
            return self.started.timestamp()
        else:
            return ""

    @property
    def endtime(self):
        if self.ended is not None:
            return self.ended.timestamp()
        else:
            return ""

    @property
    def estimated_runtime(self):
        # If there is a runtime the script has completed, no need to calculate
        # an estimate.
        if self.runtime != "":
            return self.runtime
        runtime = None
        # Get an estimated runtime from previous runs.
        for script_result in self.history.only(
                "status",
                "started",
                "ended",
                "script_id",
                "script_name",
                "script_set_id",
                "physical_blockdevice_id",
                "created",
        ):
            # Only look at passed results when calculating an estimated
            # runtime. Failed results may take longer or shorter than
            # average. Don't use self.history.filter for this as the now
            # cached history list may be used elsewhere.
            if script_result.status != SCRIPT_STATUS.PASSED:
                continue
            # LP: #1730799 - Old results may not have started set.
            if script_result.started is None:
                script_result.started = script_result.ended
                script_result.save(update_fields=["started"])
            previous_runtime = script_result.ended - script_result.started
            if runtime is None:
                runtime = previous_runtime
            else:
                runtime += previous_runtime
                runtime = runtime / 2
        if runtime is None:
            if self.script is not None and self.script.timeout != timedelta(0):
                # If there were no previous runs use the script's timeout.
                return str(self.script.timeout - timedelta(
                    microseconds=self.script.timeout.microseconds))
            else:
                return "Unknown"
        else:
            return str(runtime - timedelta(microseconds=runtime.microseconds))

    def __str__(self):
        return "%s/%s" % (self.script_set.node.system_id, self.name)

    def read_results(self):
        """Read the results YAML file and validate it."""
        try:
            parsed_yaml = yaml.safe_load(self.result)
        except yaml.YAMLError as err:
            raise ValidationError(err)

        if parsed_yaml is None:
            # No results were given.
            return {}
        elif not isinstance(parsed_yaml, dict):
            raise ValidationError("YAML must be a dictionary.")

        if parsed_yaml.get("status") not in [
                "passed",
                "failed",
                "degraded",
                "timedout",
                "skipped",
                None,
        ]:
            raise ValidationError(
                'status must be "passed", "failed", "degraded", '
                '"timedout", or "skipped".')

        link_connected = parsed_yaml.get("link_connected")
        if link_connected is not None:
            if not self.interface:
                raise ValidationError(
                    "link_connected may only be specified if the Script "
                    "accepts an interface parameter.")
            if not isinstance(link_connected, bool):
                raise ValidationError("link_connected must be a boolean")

        results = parsed_yaml.get("results")
        if results is None:
            # Results are not defined.
            return parsed_yaml
        elif isinstance(results, dict):
            for key, value in results.items():
                if not isinstance(key, str):
                    raise ValidationError(
                        "All keys in the results dictionary must be strings.")

                if not isinstance(value, list):
                    value = [value]
                for i in value:
                    if type(i) not in [str, float, int, bool]:
                        raise ValidationError(
                            "All values in the results dictionary must be "
                            "a string, float, int, or bool.")
        else:
            raise ValidationError("results must be a dictionary.")

        return parsed_yaml

    def store_result(
        self,
        exit_status=None,
        output=None,
        stdout=None,
        stderr=None,
        result=None,
        script_version_id=None,
        timedout=False,
    ):
        # Controllers and Pods are allowed to overwrite their results during any status
        # to prevent new ScriptSets being created everytime a controller
        # starts. This also allows us to avoid creating an RPC call for the
        # rack controller to create a new ScriptSet.
        if (not self.script_set.node.is_controller
                and not self.script_set.node.is_pod):
            # Allow PENDING, APPLYING_NETCONF, INSTALLING, and RUNNING scripts
            # incase the node didn't inform MAAS the Script was being run, it
            # just uploaded results.
            assert self.status in SCRIPT_STATUS_RUNNING_OR_PENDING

        if timedout:
            self.status = SCRIPT_STATUS.TIMEDOUT
        elif exit_status is not None:
            self.exit_status = exit_status
            if exit_status == 0:
                self.status = SCRIPT_STATUS.PASSED
            elif self.status == SCRIPT_STATUS.INSTALLING:
                self.status = SCRIPT_STATUS.FAILED_INSTALLING
            elif self.status == SCRIPT_STATUS.APPLYING_NETCONF:
                self.status = SCRIPT_STATUS.FAILED_APPLYING_NETCONF
            else:
                self.status = SCRIPT_STATUS.FAILED

        if output is not None:
            self.output = Bin(output)
        if stdout is not None:
            self.stdout = Bin(stdout)
        if stderr is not None:
            self.stderr = Bin(stderr)
        if result is not None:
            self.result = Bin(result)
            try:
                parsed_yaml = self.read_results()
            except ValidationError as err:
                err_msg = (
                    "%s(%s) sent a script result with invalid YAML: %s" % (
                        self.script_set.node.fqdn,
                        self.script_set.node.system_id,
                        err.message,
                    ))
                logger.error(err_msg)
                Event.objects.create_node_event(
                    system_id=self.script_set.node.system_id,
                    event_type=EVENT_TYPES.SCRIPT_RESULT_ERROR,
                    event_description=err_msg,
                )
            else:
                status = parsed_yaml.get("status")
                if status == "passed":
                    self.status = SCRIPT_STATUS.PASSED
                elif status == "failed":
                    self.status = SCRIPT_STATUS.FAILED
                elif status == "degraded":
                    self.status = SCRIPT_STATUS.DEGRADED
                elif status == "timedout":
                    self.status = SCRIPT_STATUS.TIMEDOUT
                elif status == "skipped":
                    self.status = SCRIPT_STATUS.SKIPPED

                link_connected = parsed_yaml.get("link_connected")
                if self.interface and isinstance(link_connected, bool):
                    self.interface.link_connected = link_connected
                    self.interface.save(update_fields=["link_connected"])

        if self.script:
            if script_version_id is not None:
                for script in self.script.script.previous_versions():
                    if script.id == script_version_id:
                        self.script_version = script
                        break
                if self.script_version is None:
                    err_msg = (
                        "%s(%s) sent a script result for %s(%d) with an "
                        "unknown script version(%d)." % (
                            self.script_set.node.fqdn,
                            self.script_set.node.system_id,
                            self.script.name,
                            self.script.id,
                            script_version_id,
                        ))
                    logger.error(err_msg)
                    Event.objects.create_node_event(
                        system_id=self.script_set.node.system_id,
                        event_type=EVENT_TYPES.SCRIPT_RESULT_ERROR,
                        event_description=err_msg,
                    )
            else:
                # If no script version was given assume the latest version
                # was run.
                self.script_version = self.script.script

        # If commissioning result check if its a builtin script, if so run its
        # hook before committing to the database.
        if (self.script_set.result_type == RESULT_TYPE.COMMISSIONING
                and self.name in NODE_INFO_SCRIPTS and stdout is not None):
            post_process_hook = NODE_INFO_SCRIPTS[self.name]["hook"]
            err = ("%s(%s): commissioning script '%s' failed during "
                   "post-processing." % (
                       self.script_set.node.fqdn,
                       self.script_set.node.system_id,
                       self.name,
                   ))
            # Circular imports.
            from metadataserver.api import try_or_log_event

            signal_status = try_or_log_event(
                self.script_set.node,
                None,
                err,
                post_process_hook,
                node=self.script_set.node,
                output=self.stdout,
                exit_status=self.exit_status,
            )
            # If the script failed to process mark the script as failed to
            # prevent testing from running and help users identify where
            # the error came from. This can happen when a commissioning
            # script generated invalid output.
            if signal_status is not None:
                self.status = SCRIPT_STATUS.FAILED

        if (self.status == SCRIPT_STATUS.PASSED and self.script
                and self.script.script_type == SCRIPT_TYPE.COMMISSIONING
                and self.script.recommission):
            self.script_set.scriptresult_set.filter(
                script_name__in=NODE_INFO_SCRIPTS).update(
                    status=SCRIPT_STATUS.PENDING,
                    started=None,
                    ended=None,
                    updated=now(),
                )

        self.save()

    @property
    def history(self):
        qs = ScriptResult.objects.filter(
            script_set__node_id=self.script_set.node_id)
        if self.script is not None:
            qs = qs.filter(script=self.script)
        else:
            qs = qs.filter(script_name=self.script_name)
        # XXX ltrager 2017-10-05 - Shows script runs from before MAAS supported
        # the hardware type or physical_blockdevice fields in history.
        # Solves LP: #1721524
        qs = qs.filter(
            Q(physical_blockdevice=self.physical_blockdevice)
            | Q(physical_blockdevice__isnull=True))
        qs = qs.order_by("-id")
        return qs

    def save(self, *args, **kwargs):
        if self.started is None and self.status == SCRIPT_STATUS.RUNNING:
            self.started = datetime.now()
            if "update_fields" in kwargs:
                kwargs["update_fields"].append("started")
        elif self.ended is None and self.status not in (
                SCRIPT_STATUS_RUNNING_OR_PENDING):
            self.ended = datetime.now()
            if "update_fields" in kwargs:
                kwargs["update_fields"].append("ended")
            # LP: #1730799 - If a script is run quickly the POST telling MAAS
            # the script has started comes in after the POST telling MAAS the
            # result.
            if self.started is None:
                self.started = self.ended
                if "update_fields" in kwargs:
                    kwargs["update_fields"].append("started")

        if self.id is None:
            purge_unlinked_blockdevice = False
            purge_unlinked_interface = False
            for param in self.parameters.values():
                if "value" in param and isinstance(param["value"], dict):
                    if "physical_blockdevice" in param["value"]:
                        self.physical_blockdevice = param["value"].pop(
                            "physical_blockdevice")
                        param["value"][
                            "physical_blockdevice_id"] = self.physical_blockdevice.id
                        purge_unlinked_blockdevice = True
                    elif "interface" in param["value"]:
                        self.interface = param["value"].pop("interface")
                        param["value"]["interface_id"] = self.interface.id
                        purge_unlinked_interface = True
            if True in {purge_unlinked_blockdevice, purge_unlinked_interface}:
                # Cleanup previous ScriptResults which failed to map to a
                # required device in a previous run. This may happen due to an
                # issue during commissioning such as not finding devices.
                qs = ScriptResult.objects.filter(
                    script=self.script, script_set__node=self.script_set.node)
                # Exclude passed results as they must of been from a previous
                # version of the script which did not require parameters. 2.7
                # adds interface support and the internet-connectivity test
                # has been extended to support interface parameters.
                qs = qs.exclude(status=SCRIPT_STATUS.PASSED)
                if purge_unlinked_blockdevice:
                    qs = qs.filter(physical_blockdevice=None)
                if purge_unlinked_interface:
                    qs = qs.filter(interface=None)
                qs.delete()

        return super().save(*args, **kwargs)
Beispiel #8
0
 def test_get_default_returns_Bin_from_bytes(self):
     field = BinaryField(null=True)
     self.patch(field, "default", b"wotcha")
     self.assertEqual(Bin(b"wotcha"), field.get_default())
Beispiel #9
0
 def test_get_default_returns_None(self):
     field = BinaryField(null=True)
     self.patch(field, "default", None)
     self.assertIsNone(field.get_default())
Beispiel #10
0
 def test_get_default_returns_None(self):
     field = BinaryField(null=True)
     self.patch(field, "default", None)
     self.assertIsNone(field.get_default())
Beispiel #11
0
 def test_get_default_returns_Bin_from_bytes(self):
     field = BinaryField(null=True)
     self.patch(field, "default", b"wotcha")
     self.assertEqual(Bin(b"wotcha"), field.get_default())
Beispiel #12
0
class ScriptResult(CleanSave, TimestampedModel):

    # Force model into the metadataserver namespace.
    class Meta(DefaultMeta):
        pass

    script_set = ForeignKey(ScriptSet, editable=False, on_delete=CASCADE)

    # All ScriptResults except commissioning scripts will be linked to a Script
    # as commissioning scripts are still embedded in the MAAS source.
    script = ForeignKey(
        Script, editable=False, blank=True, null=True, on_delete=SET_NULL)

    script_version = ForeignKey(
        VersionedTextFile, blank=True, null=True, editable=False,
        on_delete=SET_NULL)

    status = IntegerField(
        choices=SCRIPT_STATUS_CHOICES, default=SCRIPT_STATUS.PENDING)

    exit_status = IntegerField(blank=True, null=True)

    # Used by the builtin commissioning scripts and installation result. Also
    # stores the Script name incase the Script is deleted but the result isn't.
    script_name = CharField(
        max_length=255, unique=False, editable=False, null=True)

    output = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    stdout = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    stderr = BinaryField(max_length=1024 * 1024, blank=True, default=b'')

    # If a result is given in the output convert it to JSON and store it here.
    result = JSONObjectField(blank=True, default='')

    # When the script started to run
    started = DateTimeField(editable=False, null=True, blank=True)

    # When the script finished running
    ended = DateTimeField(editable=False, null=True, blank=True)

    @property
    def name(self):
        if self.script is not None:
            return self.script.name
        elif self.script_name is not None:
            return self.script_name
        else:
            return "Unknown"

    @property
    def status_name(self):
        return SCRIPT_STATUS_CHOICES[self.status][1]

    @property
    def runtime(self):
        if None not in (self.ended, self.started):
            runtime = self.ended - self.started
            return str(runtime - timedelta(microseconds=runtime.microseconds))
        else:
            return ''

    def __str__(self):
        return "%s/%s" % (self.script_set.node.system_id, self.name)

    def store_result(
            self, exit_status=None, output=None, stdout=None, stderr=None,
            result=None, script_version_id=None, timedout=False):
        # Don't allow ScriptResults to be overwritten unless the node is a
        # controller. Controllers are allowed to overwrite their results to
        # prevent new ScriptSets being created everytime a controller starts.
        # This also allows us to avoid creating an RPC call for the rack
        # controller to create a new ScriptSet.
        if not self.script_set.node.is_controller:
            # Allow both PENDING and RUNNING scripts incase the node didn't
            # inform MAAS the Script was being run, it just uploaded results.
            assert self.status in (
                SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING)
            assert self.output == b''
            assert self.stdout == b''
            assert self.stderr == b''
            assert self.result == ''
            assert self.script_version is None

        if timedout:
            self.status = SCRIPT_STATUS.TIMEDOUT
        elif exit_status is not None:
            self.exit_status = exit_status
            if exit_status == 0:
                self.status = SCRIPT_STATUS.PASSED
            else:
                self.status = SCRIPT_STATUS.FAILED
        if output is not None:
            self.output = Bin(output)
        if stdout is not None:
            self.stdout = Bin(stdout)
        if stderr is not None:
            self.stderr = Bin(stderr)
        if result is not None:
            self.result = result
        if self.script:
            if script_version_id is not None:
                for script in self.script.script.previous_versions():
                    if script.id == script_version_id:
                        self.script_version = script
                        break
                if self.script_version is None:
                    err_msg = (
                        "%s(%s) sent a script result for %s(%d) with an "
                        "unknown script version(%d)." % (
                            self.script_set.node.fqdn,
                            self.script_set.node.system_id,
                            self.script.name, self.script.id,
                            script_version_id))
                    logger.error(err_msg)
                    Event.objects.create_node_event(
                        system_id=self.script_set.node.system_id,
                        event_type=EVENT_TYPES.SCRIPT_RESULT_ERROR,
                        event_description=err_msg)
            else:
                # If no script version was given assume the latest version
                # was run.
                self.script_version = self.script.script

        # If commissioning result check if its a builtin script, if so run its
        # hook before committing to the database.
        if (self.script_set.result_type == RESULT_TYPE.COMMISSIONING and
                self.name in NODE_INFO_SCRIPTS):
            post_process_hook = NODE_INFO_SCRIPTS[self.name]['hook']
            post_process_hook(
                node=self.script_set.node, output=self.stdout,
                exit_status=self.exit_status)

        self.save()

    def save(self, *args, **kwargs):
        if self.started is None and self.status == SCRIPT_STATUS.RUNNING:
            self.started = datetime.now()
            if 'update_fields' in kwargs:
                kwargs['update_fields'].append('started')
        elif self.ended is None and self.status in {
                SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
                SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED}:
            self.ended = datetime.now()
            if 'update_fields' in kwargs:
                kwargs['update_fields'].append('ended')

        return super().save(*args, **kwargs)