Пример #1
0
 def _get_file(self, request, kind, filename):
     try:
         data = File(kind, self.get_object().hostname).read()
     except OSError:
         raise ParseError("Worker '%s' does not have '%s' file" %
                          (self.get_object().hostname, kind))
     response = HttpResponse(data.encode("utf-8"),
                             content_type="application/yaml")
     response["Content-Disposition"] = "attachment; filename=%s" % filename
     return response
Пример #2
0
    def show(self, hostname):
        """
        Name
        ----
        `scheduler.workers.show` (`hostname`)

        Description
        -----------
        Show some details about the given worker.

        Arguments
        ---------
        `hostname`: string
          Hostname of the worker

        Return value
        ------------
        This function returns an XML-RPC dictionary with worker details
        """

        try:
            worker = Worker.objects.get(hostname=hostname)
        except Worker.DoesNotExist:
            raise xmlrpc.client.Fault(404,
                                      "Worker '%s' was not found." % hostname)

        data = {
            "hostname":
            worker.hostname,
            "description":
            worker.description,
            "state":
            worker.get_state_display(),
            "health":
            worker.get_health_display(),
            "devices":
            [d.hostname for d in worker.device_set.all().order_by("hostname")],
            "last_ping":
            worker.last_ping,
            "job_limit":
            worker.job_limit,
            "version":
            worker.version,
            "default_config":
            not File("dispatcher", hostname).is_first(),
            "default_env":
            not File("env", hostname).is_first(),
            "default_env_dut":
            not File("env-dut", hostname).is_first(),
        }
        if self.user.is_superuser:
            data["token"] = worker.token
        return data
Пример #3
0
def test_file_device(mocker, tmpdir):
    mocker.patch(
        "lava_server.files.File.KINDS",
        {
            "device": ([str(tmpdir / "devices")], "{name}.jinja2"),
            "device-type": ([str(tmpdir / "device-types")], "{name}.jinja2"),
        },
    )
    assert File("device", "hello").exists() is False
    # Create the file
    File("device", "hello").write("hello world!")
    assert File("device", "hello").exists() is True
    assert File("device", "hello").read() == "hello world!"

    ret = File("device").list("*.jinja2")
    assert len(ret) == 1
    assert ret[0] == "hello.jinja2"

    ret = File("device").list("*.yaml")
    assert len(ret) == 0

    assert isinstance(
        File("device").loader(), jinja2.loaders.FileSystemLoader) is True
    assert File("device").loader().searchpath == [
        str(tmpdir / "devices"),
        str(tmpdir / "device-types"),
    ]
Пример #4
0
def test_file_device_type(mocker, tmpdir):
    mocker.patch(
        "lava_server.files.File.KINDS",
        {
            "device-type":
            ([str(tmpdir / "0"), str(tmpdir / "1")], "{name}.jinja2")
        },
    )
    assert File("device-type", "hello").exists() is False

    # Test fallback
    (tmpdir / "1").mkdir()
    (tmpdir / "1" / "hello.jinja2").write("base")
    assert File("device-type", "hello").read() == "base"

    # Create the file
    File("device-type", "hello").write("new version")
    assert File("device-type", "hello").exists() is True
    assert File("device-type", "hello").read() == "new version"

    ret = File("device-type").list("*.yaml")
    assert len(ret) == 0

    ret = File("device-type").list("*.jinja2")
    assert len(ret) == 1
    assert ret[0] == "hello.jinja2"

    assert File("device-type").loader().searchpath == [
        str(tmpdir / "0"),
        str(tmpdir / "1"),
    ]
Пример #5
0
    def set_env(self, hostname, env):
        """
        Name
        ----
        `scheduler.workers.set_env` (`hostname`, `env`)

        Description
        -----------
        Set the worker environment

        Arguments
        ---------
        `hostname`: string
          Hostname of the worker
        `env`: string
          The worker environment as a yaml file

        Return value
        ------------
        True if the environment was saved to file, False otherwise.
        """
        # Sanitize hostname as we will use it in a path
        if len(pathlib.Path(hostname).parts) != 1:
            raise xmlrpc.client.Fault(400, "Invalid worker name")

        try:
            Worker.objects.get(hostname=hostname)
        except Worker.DoesNotExist:
            raise xmlrpc.client.Fault(404, "Worker '%s' was not found." % hostname)

        with contextlib.suppress(OSError):
            File("env", hostname).write(env)
            return True

        return False
Пример #6
0
    def health_check(self, request, **kwargs):
        if request.method == "GET":
            if not self.get_object().can_view(request.user):
                raise Http404(
                    "Device-type '%s' was not found." % self.get_object().name
                )

            try:
                response = HttpResponse(
                    File("health-check", self.get_object().name).read().encode("utf-8"),
                    content_type="application/yaml",
                )
                response["Content-Disposition"] = (
                    "attachment; filename=%s_health_check.yaml" % self.get_object().name
                )
                return response
            except FileNotFoundError:
                raise ParseError(
                    "Device-type '%s' health check was not found."
                    % self.get_object().name
                )
            except OSError as exc:
                raise ParseError(
                    "Unable to read health check configuration: %s" % exc.strerror
                )

        elif request.method == "POST":
            if not request.user.has_perm("lava_scheduler_app.change_device"):
                raise PermissionDenied(
                    "Insufficient permissions. Please contact system administrator."
                )
            config = request.data.get("config", None)
            if not config:
                raise ValidationError(
                    {"config": "Health check configuration is required."}
                )

            try:
                File("health-check", self.get_object().name).write(config)
                return Response(
                    {"message": "health check updated"},
                    status=status.HTTP_204_NO_CONTENT,
                )
            except OSError as exc:
                raise ParseError(
                    "Unable to write health check configuration: %s" % exc.strerror
                )
Пример #7
0
def test_file_env(mocker, tmpdir):
    mocker.patch(
        "lava_server.files.File.KINDS",
        {"env": [str(tmpdir / "{name}/env.yaml"),
                 str(tmpdir / "env.yaml")]},
    )
    assert File("env", "worker01").exists() is False

    # Test fallback
    (tmpdir / "worker01").mkdir()
    (tmpdir / "worker01" / "env.yaml").write("base")
    assert File("env", "worker01").read() == "base"

    # Create the file
    File("env", "worker01").write("new version")
    assert File("env", "worker01").exists() is True
    assert File("env", "worker01").read() == "new version"
Пример #8
0
def devices():
    thread_locals = threading.local()
    try:
        return thread_locals.devices
    except AttributeError:
        thread_locals.devices = jinja2.Environment(
            loader=File("device").loader(), autoescape=False, trim_blocks=True)
    return thread_locals.devices
Пример #9
0
 def _available_device_types(self):
     """ List avaiable device types by looking at the configuration files """
     available_types = []
     for device_type in File("device-type").list("*.jinja2"):
         if not device_type.startswith("base"):
             available_types.append(device_type[:-7])
     available_types.sort()
     return available_types
Пример #10
0
    def _parse_config(self, hostname):
        # Will raise OSError if the file does not exist.
        # Will raise jinja2.TemplateError if the template cannot be parsed.
        jinja_config = File("device", hostname).read()

        env = jinja2.Environment(  # nosec - YAML, not HTML, no XSS scope.
            autoescape=False)
        ast = env.parse(jinja_config)
        return ast
Пример #11
0
def config(kind, worker):
    try:
        data = File(kind, worker.hostname).read(raising=False)
        yaml_safe_load(data)
        return data
    except yaml.YAMLError:
        # Raise an OSError because the caller uses yaml.YAMLError for a
        # specific usage. Allows here to specify the faulty filename.
        raise OSError("",
                      f"Invalid YAML file for {worker.hostname}: {kind} file")
Пример #12
0
    def test_device_type_templates(self):
        """
        Ensure each template renders valid YAML
        """
        env = jinja2.Environment(  # nosec - YAML, not HTML, no XSS scope.
            loader=File("device-type").loader(), trim_blocks=True, autoescape=False
        )

        for template_name in File("device-type").list("*.jinja2"):
            try:
                template = env.get_template(template_name.name)
            except jinja2.TemplateNotFound as exc:
                self.fail("%s: %s" % (template_name, exc))
            data = None
            try:
                data = template.render()
                yaml_data = yaml_safe_load(data)
            except yaml.YAMLError as exc:
                print(data)  # for easier debugging - use the online yaml parser
                self.fail("%s: %s" % (template_name, exc))
            self.assertIsInstance(yaml_data, dict)
Пример #13
0
    def template(self, request, **kwargs):
        if request.method == "GET":
            if not self.get_object().can_view(request.user):
                raise Http404("Device-type '%s' was not found." %
                              self.get_object().name)

            try:
                filename = f"{self.get_object().name}.jinja2"
                response = HttpResponse(
                    File("device-type").read(filename).encode("utf-8"),
                    content_type="application/yaml",
                )
                response["Content-Disposition"] = (
                    "attachment; filename=%s.jinja2" % self.get_object().name)
                return response
            except FileNotFoundError:
                raise ParseError("Device-type '%s' template was not found." %
                                 self.get_object().name)
            except OSError as exc:
                raise ParseError(
                    "Unable to read device-type configuration: %s" %
                    exc.strerror)

        elif request.method == "POST":
            if not request.user.has_perm("lava_scheduler_app.change_device"):
                raise PermissionDenied(
                    "Insufficient permissions. Please contact system administrator."
                )
            template = request.data.get("template", None)
            if not template:
                raise ValidationError(
                    {"template": "Device type template is required."})
            try:
                filename = f"{self.get_object().name}.jinja2"
                File("device-type").write(filename, template)
                return Response({"message": "template updated"},
                                status=status.HTTP_204_NO_CONTENT)
            except OSError as exc:
                raise ParseError("Unable to write device-type template: %s" %
                                 exc.strerror)
Пример #14
0
def test_file_errors(mocker, tmpdir):
    mocker.patch(
        "lava_server.files.File.KINDS",
        {
            "health-check":
            ([str(tmpdir / "0"), str(tmpdir / "1")], "{name}.jinja2"),
            "env": [str(tmpdir / "env")],
        },
    )

    with pytest.raises(NotImplementedError):
        File("health-checks")

    with pytest.raises(NotImplementedError):
        File("health-check").loader()

    with pytest.raises(NotImplementedError):
        File("env").list("*.yaml")

    with pytest.raises(FileNotFoundError):
        File("health-check", "docker").read()
    assert File("health-check", "docker").read(raising=False) == ""
Пример #15
0
 def test_bbb_valid_template(self):
     name = "beaglebone-black"
     dt = DeviceType(name=name)
     dt.save()
     dt.refresh_from_db()
     device = Device(device_type=dt, hostname="bbb-01", health=Device.HEALTH_GOOD)
     device.save()
     device.refresh_from_db()
     self.assertIsNotNone([d for d in Device.objects.filter(device_type=dt)])
     self.assertTrue(File("device-type").exists(f"{name}.jinja2"))
     self.assertIsNotNone([device in Device.objects.filter(device_type=dt)])
     self.assertIsNotNone(device.load_configuration())
     self.assertTrue(bool(load_devicetype_template(device.device_type.name)))
     self.assertFalse(invalid_template(device.device_type))
Пример #16
0
    def show(self, name):
        """
        Name
        ----
        `scheduler.device_types.show` (`name`)

        Description
        -----------
        Show some details about the given device type.

        Arguments
        ---------
        `name`: string
          Name of the device-type

        Return value
        ------------
        This function returns an XML-RPC dictionary with device-type details
        """

        try:
            dt = DeviceType.objects.get(name=name)
        except DeviceType.DoesNotExist:
            raise xmlrpc.client.Fault(404,
                                      "Device-type '%s' was not found." % name)
        if not dt.can_view(self.user):
            raise xmlrpc.client.Fault(404,
                                      "Device-type '%s' was not found." % name)

        aliases = [str(alias.name) for alias in dt.aliases.all()]
        devices = [
            str(d.hostname) for d in dt.device_set.all()
            if d.can_view(self.user)
        ]
        dt_dict = {
            "name": dt.name,
            "description": dt.description,
            "display": dt.display,
            "health_disabled": dt.disable_health_check,
            "health_denominator": dt.get_health_denominator_display(),
            "health_frequency": dt.health_frequency,
            "aliases": aliases,
            "devices": devices,
            "default_template": not File("device-type", name).is_first(),
        }

        return dt_dict
Пример #17
0
    def get_template(self, name):
        """
        Name
        ----
        `scheduler.device_types.get_template` (`name`)

        Description
        -----------
        Return the device-type configuration for the requested device-type or
        filename.

        Note: not all device-types have a health check filename that matches
        the device-type name in the database.

        Arguments
        ---------
        `name`: string
          Name of the device-type

        The .jinja2 suffix will be added if not specified.

        Return value
        ------------
        The device-type configuration
        """
        with contextlib.suppress(DeviceType.DoesNotExist):
            dt = DeviceType.objects.get(name=name)
            if not dt.can_view(self.user):
                raise xmlrpc.client.Fault(
                    404, "Device-type '%s' was not found." % name)

        # Filename should not be a path or starting with a dot
        if os.path.basename(name) != name or name[0] == ".":
            raise xmlrpc.client.Fault(400, "Invalid device-type '%s'" % name)

        try:
            return xmlrpc.client.Binary(
                File("device-type", name).read().encode("utf-8"))
        except FileNotFoundError:
            raise xmlrpc.client.Fault(404,
                                      "Device-type '%s' was not found." % name)
        except OSError as exc:
            raise xmlrpc.client.Fault(
                400,
                "Unable to read device-type configuration: %s" % exc.strerror)
Пример #18
0
    def get_config(self, hostname):
        """
        Name
        ----
        `scheduler.workers.get_config` (`hostname`)

        Description
        -----------
        Return the worker configuration
        The server will first try
        /etc/lava-server/dispatcher.d/<hostname>/dispatcher.yaml and fallback to
        /etc/lava-server/dispatcher.d/<hostname>.yaml

        Arguments
        ---------
        `hostname`: string
          Hostname of the worker

        Return value
        ------------
        This function returns the worker configuration
        """
        # Sanitize hostname as we will use it in a path
        if len(pathlib.Path(hostname).parts) != 1:
            raise xmlrpc.client.Fault(400, "Invalid worker name")

        # Find the worker in the database
        try:
            Worker.objects.get(hostname=hostname)
        except Worker.DoesNotExist:
            raise xmlrpc.client.Fault(404, "Worker '%s' was not found." % hostname)

        try:
            return xmlrpc.client.Binary(
                File("dispatcher", hostname).read().encode("utf-8")
            )
        except FileNotFoundError:
            raise xmlrpc.client.Fault(
                404, "Worker '%s' does not have a dispatcher configuration" % hostname
            )
        except OSError as exc:
            raise xmlrpc.client.Fault(
                400, "Unable to read dispatcher configuration: %s" % exc.strerror
            )
Пример #19
0
 def test_juno_vexpress_valid_template(self):
     name = "juno-r2"
     dt = DeviceType(name=name)
     dt.save()
     dt.refresh_from_db()
     device = Device(
         device_type=dt, hostname="juno-r2-01", health=Device.HEALTH_GOOD
     )
     device.save()
     device.refresh_from_db()
     self.assertIsNotNone([d for d in Device.objects.filter(device_type=dt)])
     self.assertFalse(File("device-type").exists("juno-r2.jinja2"))
     self.assertEqual("juno-r2-01", device.hostname)
     self.assertIsNotNone(device.load_configuration())
     self.assertEqual(
         [device], [device for device in Device.objects.filter(device_type=dt)]
     )
     self.assertEqual("juno", device.get_extends())
     self.assertFalse(bool(load_devicetype_template(device.device_type.name)))
     self.assertFalse(invalid_template(device.device_type))
Пример #20
0
    def set_template(self, name, config):
        """
        Name
        ----
        `scheduler.device_types.set_template` (`name`, `config`)

        Description
        -----------
        [superuser only]
        Set the device-type configuration for the requested device-type or
        filename.

        Note: not all device-types have a health check filename that matches
        the device-type name in the database.

        Arguments
        ---------
        `name`: string
          name of the device-type
        `config`: string
          The device-type configuration as a jinja2 template

        The .jinja2 suffix will be added if not specified.

        Return value
        ------------
        None
        """
        # Filename should not be a path or starting with a dot
        if os.path.basename(name) != name or name[0] == ".":
            raise xmlrpc.client.Fault(400, "Invalid device-type '%s'" % name)

        try:
            name = name if name.endswith(".jinja2") else name + ".jinja2"
            File("device-type").write(name, config)
        except OSError as exc:
            raise xmlrpc.client.Fault(
                400, "Unable to write device-type configuration: %s" % exc.strerror
            )
Пример #21
0
    def validate(self, request, **kwargs):
        """
        Takes a string of a device dictionary to validate if it can be
        rendered and loaded correctly.
        """
        devicedict = request.data
        if not devicedict:
            raise ValidationError({"device": "Device dictionary is required."})

        try:
            template = jinja2.Environment(
                loader=File("device").loader(),
                autoescape=False,
                trim_blocks=True).from_string(devicedict)
            yaml_safe_load(template.render())
            return Response({"message": "Device dictionary valid."},
                            status=status.HTTP_200_OK)
        except Exception as exc:
            return Response(
                {"message": "Device dictionary invalid: %s" % str(exc)},
                status=status.HTTP_200_OK,
            )
Пример #22
0
    def set_health_check(self, name, config):
        """
        Name
        ----
        `scheduler.device_types.set_health_check` (`name`, `config`)

        Description
        -----------
        [superuser only]
        Set the health-check definition for the requested device-type or
        filename.

        Note: not all device-types have a health check filename that matches
        the device-type name in the database.

        Arguments
        ---------
        `name`: string
          name of the device-type
        `config`: string
          The health-check as a yaml file

        The .yaml suffix will be added if not specified.

        Return value
        ------------
        None
        """
        # Filename should not be a path or starting with a dot
        if os.path.basename(name) != name or name[0] == ".":
            raise xmlrpc.client.Fault(400, "Invalid device-type '%s'" % name)

        try:
            File("health-check", name).write(config)
        except OSError as exc:
            raise xmlrpc.client.Fault(
                400, "Unable to write health-check: %s" % exc.strerror)
Пример #23
0
#
# LAVA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with LAVA.  If not, see <http://www.gnu.org/licenses/>.

import jinja2
import threading

from lava_server.files import File

thread_locals = threading.local()

thread_locals.devices = jinja2.Environment(loader=File("device").loader(),
                                           autoescape=False,
                                           trim_blocks=True)

thread_locals.device_types = jinja2.Environment(
    loader=File("device-type").loader(), autoescape=False, trim_blocks=True)


def devices():
    return thread_locals.devices


def device_types():
    return thread_locals.device_types
Пример #24
0
    def handle_copy(self, options):
        original = options["original"]
        target = options["target"]
        worker_name = options["worker"]
        public = options["public"]
        online = options["online"]
        tags = options["copytags"]

        try:
            from_device = Device.objects.get(hostname=original)
        except Device.DoesNotExist:
            raise CommandError("Original device '%s' does NOT exist!" %
                               original)

        with contextlib.suppress(Device.DoesNotExist):
            Device.objects.get(hostname=target)
            raise CommandError("Target device '%s' already exists" % target)

        origin = from_device.load_configuration()
        if not origin:
            raise CommandError("Device dictionary does not exist for %s" %
                               original)

        if online:
            if not File("device").exists(f"{target}.jinja2"):
                raise CommandError(
                    "Refusing to copy %s to new device %s with health 'Good' -"
                    " no device dictionary exists for target device, yet. "
                    "Use --offline or copy %s.jinja2 and try again." %
                    (original, target, target))

        try:
            worker = Worker.objects.get(hostname=worker_name)
        except Worker.DoesNotExist:
            raise CommandError("Unable to find worker '%s'" % worker_name)

        health = Device.HEALTH_GOOD if online else Device.HEALTH_MAINTENANCE
        description = from_device.description
        device_type = from_device.device_type
        from_tags = None
        if tags:
            from_tags = from_device.tags.all()

        physical_owner = None
        physical_group = None
        owner = None
        group = None

        if from_device.physical_owner:
            physical_owner = from_device.physical_owner
        elif from_device.physical_group:
            physical_group = from_device.physical_group

        if from_device.owner:
            owner = from_device.owner
        elif from_device.group:
            group = from_device.group

        with transaction.atomic():
            device = Device.objects.create(
                hostname=target,
                device_type=device_type,
                description=description,
                state=Device.STATE_IDLE,
                health=health,
                worker_host=worker,
            )

            if from_tags is not None:
                for tag in from_tags:
                    device.tags.add(tag)

            if physical_owner:
                self._assign(physical_owner, device, user=True, physical=True)
            elif physical_group:
                self._assign(physical_group, device, group=True, physical=True)

            if owner:
                self._assign(owner, device, user=True, owner=True)
            elif group:
                self._assign(group, device, group=True, owner=True)

            device.save()

        destiny = device.load_configuration()
        if not destiny:
            print("Reminder: device dictionary does not yet exist for %s" %
                  target)
Пример #25
0
    def handle(self, *_, **options):
        dicts = File("device").list("*.jinja2")
        synced_devices = []
        self.stdout.write("Scanning devices:")
        for name in dicts:
            hostname = name.rsplit(".", 1)[0]

            # Get value of 'sync_to_lava' variable from template.
            sync_dict = self._get_sync_to_lava(hostname)
            if sync_dict is None:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> missing '{self.SYNC_KEY}'")
                continue
            # Convert it to dictionary.
            sync_dict = self._parse_sync_dict(sync_dict)

            try:
                template = environment.devices().get_template(name)
                device_template = yaml_safe_load(template.render())
            except jinja2.TemplateError as exc:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> invalid jinja2 template")
                self.stdout.write(f"  -> {exc}")
                continue
            except yaml.YAMLError as exc:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> invalid yaml")
                self.stdout.write(f"  -> {exc}")
                continue

            # Check if this device is already manually created in db.
            with contextlib.suppress(Device.DoesNotExist):
                device = Device.objects.get(hostname=hostname)
                if not device.is_synced:
                    self.stdout.write(f"* {hostname} [SKIP]")
                    self.stdout.write(f"  -> created manually")
                    continue

            # Check keys
            if "device_type" not in sync_dict:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> 'device_type' is mandatory")
                continue

            # Add to managed devices list.
            self.stdout.write(f"* {hostname}")
            synced_devices.append(hostname)

            # Create device type. If not found, report an error and skip.
            device_type, created = DeviceType.objects.get_or_create(
                name=sync_dict["device_type"])
            if created:
                self.stdout.write(
                    f"  -> create device type: {device_type.name}")

            worker = None
            if "worker" in sync_dict:
                worker, created = Worker.objects.get_or_create(
                    hostname=sync_dict["worker"])
                if created:
                    self.stdout.write(
                        f"  -> create worker: {sync_dict['worker']}")

            # Create/update device.
            defaults = {
                "device_type": device_type,
                "description": "Created automatically by LAVA.",
                "worker_host": worker,
                "is_synced": True,
            }
            device, created = Device.objects.update_or_create(
                defaults, hostname=hostname)
            if created:
                Device.objects.filter(hostname=hostname).update(
                    health=Device.HEALTH_UNKNOWN)

            # Create aliases and tags.
            for alias_name in sync_dict.get("aliases", []):
                Alias.objects.get_or_create(name=alias_name,
                                            device_type=device_type)
                self.stdout.write(f"  -> alias: {alias_name}")
            for tag_name in sync_dict.get("tags", []):
                tag, _ = Tag.objects.get_or_create(name=tag_name)
                device.tags.add(tag)
                self.stdout.write(f"  -> tag: {tag_name}")

        # devices which have is_synced true if there's no device dict for them.
        Device.objects.filter(is_synced=True).exclude(
            hostname__in=synced_devices).update(health=Device.HEALTH_RETIRED)

        # Device types which have all the devices synced and all of them retired
        # should become invisible.
        dts = (DeviceType.objects.annotate(not_synced_retired_count=Count(
            Case(
                When(
                    Q(device__is_synced=False)
                    | ~Q(device__health=Device.HEALTH_RETIRED),
                    then=1,
                ),
                output_field=IntegerField(),
            ))).filter(not_synced_retired_count=0).update(display=False))
Пример #26
0
    def handle(self, *_, **options):
        dicts = File("device").list("*.jinja2")
        synced_devices = []
        self.stdout.write("Scanning devices:")
        for name in dicts:
            hostname = name.rsplit(".", 1)[0]

            # Get value of 'sync_to_lava' variable from template.
            sync_dict, exc = self._get_sync_to_lava(hostname)

            if exc:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> invalid jinja2 template")
                self.stdout.write(f"  -> {exc}")
                continue

            if sync_dict is None:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> missing '{self.SYNC_KEY}'")
                continue

            # Convert it to dictionary.
            sync_dict = self._parse_sync_dict(sync_dict)

            try:
                template = environment.devices().get_template(name)
                device_template = yaml_safe_load(template.render())
            except jinja2.TemplateError as exc:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> invalid jinja2 template")
                self.stdout.write(f"  -> {exc}")
                continue
            except yaml.YAMLError as exc:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> invalid yaml")
                self.stdout.write(f"  -> {exc}")
                continue

            # Check if this device is already manually created in db.
            with contextlib.suppress(Device.DoesNotExist):
                device = Device.objects.get(hostname=hostname)
                if not device.is_synced:
                    self.stdout.write(f"* {hostname} [SKIP]")
                    self.stdout.write(f"  -> created manually")
                    continue

            # Check keys
            if "device_type" not in sync_dict:
                self.stdout.write(f"* {hostname} [SKIP]")
                self.stdout.write(f"  -> 'device_type' is mandatory")
                continue

            # Add to managed devices list.
            self.stdout.write(f"* {hostname}")
            synced_devices.append(hostname)

            # Create device type. If not found, report an error and skip.
            device_type, created = DeviceType.objects.get_or_create(
                name=sync_dict["device_type"])
            if created:
                self.stdout.write(
                    f"  -> create device type: {device_type.name}")

            worker = None
            if "worker" in sync_dict:
                worker, created = Worker.objects.get_or_create(
                    hostname=sync_dict["worker"])
                if created:
                    self.stdout.write(
                        f"  -> create worker: {sync_dict['worker']}")

            # Create/update device.
            defaults = {
                "device_type": device_type,
                "description": "Created automatically by LAVA.",
                "worker_host": worker,
                "is_synced": True,
            }
            device, created = Device.objects.update_or_create(
                defaults, hostname=hostname)
            if created:
                Device.objects.filter(hostname=hostname).update(
                    health=Device.HEALTH_UNKNOWN)

            # Create aliases.
            for alias_name in sync_dict.get("aliases", []):
                Alias.objects.get_or_create(name=alias_name,
                                            device_type=device_type)
                self.stdout.write(f"  -> alias: {alias_name}")

            # Remove all tag relations first.
            device.tags.clear()
            # Create tags.
            for tag_name in sync_dict.get("tags", []):
                tag, _ = Tag.objects.get_or_create(name=tag_name)
                device.tags.add(tag)
                self.stdout.write(f"  -> tag: {tag_name}")

            # Link physical owner
            specified_owner = sync_dict.get("physical_owner", "")
            try:
                physical_owner = User.objects.get(username=specified_owner)
                device.physical_owner = physical_owner
                self.stdout.write(f"  -> user: {specified_owner}")
            except User.DoesNotExist:
                device.physical_owner = None
                if specified_owner:
                    self.stdout.write(
                        f"  -> user '{specified_owner}' does not exist")
            finally:
                device.save()

            # Link physical group
            specified_group = sync_dict.get("physical_group", "")
            try:
                physical_group = Group.objects.get(name=specified_group)
                device.physical_group = physical_group
                self.stdout.write(f"  -> group: {specified_group}")
            except Group.DoesNotExist:
                device.physical_group = None
                if specified_group:
                    self.stdout.write(
                        f"  -> group '{specified_group}' does not exist")
            finally:
                device.save()

            # Assign permission
            specified_permissions = sync_dict.get("group_device_permissions",
                                                  [])
            for permission in specified_permissions:
                perm = permission[0]
                group = permission[1]

                try:
                    permission_group = Group.objects.get(name=group)
                    try:
                        GroupDevicePermission.objects.assign_perm(
                            perm, permission_group, device)
                        self.stdout.write(
                            f"  -> add group permission: ({perm}, {group})")
                    except PermissionNameError:
                        self.stdout.write(
                            f"  -> permission '{perm}' does not exist")
                except Group.DoesNotExist:
                    self.stdout.write(f"  -> group '{group}' does not exist")

            # Delete unused permission
            kwargs = {"device": device}
            obj_perm = GroupDevicePermission.objects.filter(**kwargs)
            for perm in obj_perm:
                if [
                        perm.permission.codename,
                        perm.group.name,
                ] not in specified_permissions:
                    GroupDevicePermission.objects.remove_perm(
                        perm.permission.codename, perm.group, perm.device)
                    self.stdout.write(
                        f"  -> delete group permission: ({perm.permission.codename}, {perm.group.name})"
                    )

        # devices which have is_synced true if there's no device dict for them.
        Device.objects.filter(is_synced=True).exclude(
            hostname__in=synced_devices).update(health=Device.HEALTH_RETIRED)

        # Device types which have all the devices synced and all of them retired
        # should become invisible.
        synced_retired_queryset = DeviceType.objects.annotate(
            not_synced_retired_count=Count(
                Case(
                    When(
                        Q(device__is_synced=False)
                        | ~Q(device__health=Device.HEALTH_RETIRED),
                        then=1,
                    ),
                    output_field=IntegerField(),
                )))
        synced_retired_queryset.filter(not_synced_retired_count=0).update(
            display=False)

        # Device types which have all the devices synced and some of them not
        # retired should become visible.
        synced_not_retired_queryset = DeviceType.objects.annotate(
            not_synced=Count(
                Case(
                    When(Q(device__is_synced=False), then=1),
                    output_field=IntegerField(),
                )),
            not_retired=Count(
                Case(
                    When(~Q(device__health=Device.HEALTH_RETIRED), then=1),
                    output_field=IntegerField(),
                )),
        )
        synced_not_retired_queryset.filter(not_synced=0).filter(
            not_retired__gt=0).update(display=True)
Пример #27
0
 def device_types():
     return jinja2.Environment(
         loader=File("device-type").loader(), autoescape=False, trim_blocks=True
     )