Merge remote-tracking branch 'origin/pr/296'
* origin/pr/296: ext/block: prefer connecting cdrom as xvdd tests: extend mock objects in QubesVM tests
This commit is contained in:
commit
09ab00cafd
@ -37,7 +37,9 @@ mode_re = re.compile(r"^[rw]$")
|
|||||||
AVAILABLE_FRONTENDS = ['xvd'+c for c in
|
AVAILABLE_FRONTENDS = ['xvd'+c for c in
|
||||||
string.ascii_lowercase[8:]+string.ascii_lowercase[:8]]
|
string.ascii_lowercase[8:]+string.ascii_lowercase[:8]]
|
||||||
|
|
||||||
SYSTEM_DISKS = ('xvda', 'xvdb', 'xvdc', 'xvdd')
|
SYSTEM_DISKS = ('xvda', 'xvdb', 'xvdc')
|
||||||
|
# xvdd is considered system disk only if vm.kernel is set
|
||||||
|
SYSTEM_DISKS_DOM0_KERNEL = SYSTEM_DISKS + ('xvdd',)
|
||||||
|
|
||||||
|
|
||||||
class BlockDevice(qubes.devices.DeviceInfo):
|
class BlockDevice(qubes.devices.DeviceInfo):
|
||||||
@ -172,6 +174,9 @@ class BlockDeviceExtension(qubes.ext.Extension):
|
|||||||
if not vm.is_running():
|
if not vm.is_running():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
system_disks = SYSTEM_DISKS
|
||||||
|
if getattr(vm, 'kernel', None):
|
||||||
|
system_disks = SYSTEM_DISKS_DOM0_KERNEL
|
||||||
xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
|
xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
|
||||||
|
|
||||||
for disk in xml_desc.findall('devices/disk'):
|
for disk in xml_desc.findall('devices/disk'):
|
||||||
@ -187,7 +192,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
|
|||||||
frontend_dev = target_node.get('dev')
|
frontend_dev = target_node.get('dev')
|
||||||
if not frontend_dev:
|
if not frontend_dev:
|
||||||
continue
|
continue
|
||||||
if frontend_dev in SYSTEM_DISKS:
|
if frontend_dev in system_disks:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@ -217,7 +222,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
|
|||||||
|
|
||||||
yield (BlockDevice(backend_domain, ident), options)
|
yield (BlockDevice(backend_domain, ident), options)
|
||||||
|
|
||||||
def find_unused_frontend(self, vm):
|
def find_unused_frontend(self, vm, devtype='disk'):
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
'''Find unused block frontend device node for <target dev=.../>
|
'''Find unused block frontend device node for <target dev=.../>
|
||||||
parameter'''
|
parameter'''
|
||||||
@ -227,6 +232,10 @@ class BlockDeviceExtension(qubes.ext.Extension):
|
|||||||
parsed_xml = lxml.etree.fromstring(xml)
|
parsed_xml = lxml.etree.fromstring(xml)
|
||||||
used = [target.get('dev', None) for target in
|
used = [target.get('dev', None) for target in
|
||||||
parsed_xml.xpath("//domain/devices/disk/target")]
|
parsed_xml.xpath("//domain/devices/disk/target")]
|
||||||
|
if devtype == 'cdrom' and 'xvdd' not in used:
|
||||||
|
# prefer 'xvdd' for CDROM if available; only first 4 disks are
|
||||||
|
# emulated in HVM, which means only those are bootable
|
||||||
|
return 'xvdd'
|
||||||
for dev in AVAILABLE_FRONTENDS:
|
for dev in AVAILABLE_FRONTENDS:
|
||||||
if dev not in used:
|
if dev not in used:
|
||||||
return dev
|
return dev
|
||||||
@ -269,7 +278,8 @@ class BlockDeviceExtension(qubes.ext.Extension):
|
|||||||
'it'.format(device.backend_domain.name))
|
'it'.format(device.backend_domain.name))
|
||||||
|
|
||||||
if 'frontend-dev' not in options:
|
if 'frontend-dev' not in options:
|
||||||
options['frontend-dev'] = self.find_unused_frontend(vm)
|
options['frontend-dev'] = self.find_unused_frontend(
|
||||||
|
vm, options.get('devtype', 'disk'))
|
||||||
|
|
||||||
vm.libvirt_domain.attachDevice(
|
vm.libvirt_domain.attachDevice(
|
||||||
vm.app.env.get_template('libvirt/devices/block.xml').render(
|
vm.app.env.get_template('libvirt/devices/block.xml').render(
|
||||||
|
@ -25,6 +25,15 @@ import jinja2
|
|||||||
import qubes.tests
|
import qubes.tests
|
||||||
import qubes.ext.block
|
import qubes.ext.block
|
||||||
|
|
||||||
|
modules_disk = '''
|
||||||
|
<disk type='block' device='disk'>
|
||||||
|
<driver name='phy'/>
|
||||||
|
<source dev='/var/lib/qubes/vm-kernels/4.4.55-11/modules.img'/>
|
||||||
|
<backingStore/>
|
||||||
|
<target dev='xvdd' bus='xen'/>
|
||||||
|
<readonly/>
|
||||||
|
</disk>
|
||||||
|
'''
|
||||||
|
|
||||||
domain_xml_template = '''
|
domain_xml_template = '''
|
||||||
<domain type='xen' id='9'>
|
<domain type='xen' id='9'>
|
||||||
@ -66,13 +75,6 @@ domain_xml_template = '''
|
|||||||
<backingStore/>
|
<backingStore/>
|
||||||
<target dev='xvdc' bus='xen'/>
|
<target dev='xvdc' bus='xen'/>
|
||||||
</disk>
|
</disk>
|
||||||
<disk type='block' device='disk'>
|
|
||||||
<driver name='phy'/>
|
|
||||||
<source dev='/var/lib/qubes/vm-kernels/4.4.55-11/modules.img'/>
|
|
||||||
<backingStore/>
|
|
||||||
<target dev='xvdd' bus='xen'/>
|
|
||||||
<readonly/>
|
|
||||||
</disk>
|
|
||||||
{}
|
{}
|
||||||
<interface type='ethernet'>
|
<interface type='ethernet'>
|
||||||
<mac address='00:16:3e:5e:6c:06'/>
|
<mac address='00:16:3e:5e:6c:06'/>
|
||||||
@ -486,6 +488,46 @@ class TC_00_Block(qubes.tests.QubesTestCase):
|
|||||||
'</disk>')
|
'</disk>')
|
||||||
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
|
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
|
||||||
|
|
||||||
|
def test_048_attach_cdrom_xvdi(self):
|
||||||
|
back_vm = TestVM(name='sys-usb', qdb={
|
||||||
|
'/qubes-block-devices/sda': b'',
|
||||||
|
'/qubes-block-devices/sda/desc': b'Test device',
|
||||||
|
'/qubes-block-devices/sda/size': b'1024000',
|
||||||
|
'/qubes-block-devices/sda/mode': b'r',
|
||||||
|
})
|
||||||
|
vm = TestVM({}, domain_xml=domain_xml_template.format(modules_disk))
|
||||||
|
dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
|
||||||
|
self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'})
|
||||||
|
device_xml = (
|
||||||
|
'<disk type="block" device="cdrom">\n'
|
||||||
|
' <driver name="phy" />\n'
|
||||||
|
' <source dev="/dev/sda" />\n'
|
||||||
|
' <target dev="xvdi" />\n'
|
||||||
|
' <readonly />\n'
|
||||||
|
' <backenddomain name="sys-usb" />\n'
|
||||||
|
'</disk>')
|
||||||
|
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
|
||||||
|
|
||||||
|
def test_048_attach_cdrom_xvdd(self):
|
||||||
|
back_vm = TestVM(name='sys-usb', qdb={
|
||||||
|
'/qubes-block-devices/sda': b'',
|
||||||
|
'/qubes-block-devices/sda/desc': b'Test device',
|
||||||
|
'/qubes-block-devices/sda/size': b'1024000',
|
||||||
|
'/qubes-block-devices/sda/mode': b'r',
|
||||||
|
})
|
||||||
|
vm = TestVM({}, domain_xml=domain_xml_template.format(''))
|
||||||
|
dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
|
||||||
|
self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'})
|
||||||
|
device_xml = (
|
||||||
|
'<disk type="block" device="cdrom">\n'
|
||||||
|
' <driver name="phy" />\n'
|
||||||
|
' <source dev="/dev/sda" />\n'
|
||||||
|
' <target dev="xvdd" />\n'
|
||||||
|
' <readonly />\n'
|
||||||
|
' <backenddomain name="sys-usb" />\n'
|
||||||
|
'</disk>')
|
||||||
|
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
|
||||||
|
|
||||||
def test_050_detach(self):
|
def test_050_detach(self):
|
||||||
back_vm = TestVM(name='sys-usb', qdb={
|
back_vm = TestVM(name='sys-usb', qdb={
|
||||||
'/qubes-block-devices/sda': b'',
|
'/qubes-block-devices/sda': b'',
|
||||||
|
@ -39,6 +39,7 @@ import shutil
|
|||||||
import qubes
|
import qubes
|
||||||
import qubes.exc
|
import qubes.exc
|
||||||
import qubes.config
|
import qubes.config
|
||||||
|
import qubes.devices
|
||||||
import qubes.vm
|
import qubes.vm
|
||||||
import qubes.vm.qubesvm
|
import qubes.vm.qubesvm
|
||||||
|
|
||||||
@ -79,8 +80,10 @@ class TestDeviceCollection(object):
|
|||||||
return self._list
|
return self._list
|
||||||
|
|
||||||
class TestQubesDB(object):
|
class TestQubesDB(object):
|
||||||
def __init__(self):
|
def __init__(self, data=None):
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
if data:
|
||||||
|
self.data = data
|
||||||
|
|
||||||
def write(self, path, value):
|
def write(self, path, value):
|
||||||
self.data[path] = value
|
self.data[path] = value
|
||||||
@ -92,6 +95,12 @@ class TestQubesDB(object):
|
|||||||
else:
|
else:
|
||||||
self.data.pop(path, None)
|
self.data.pop(path, None)
|
||||||
|
|
||||||
|
def list(self, prefix):
|
||||||
|
return [key for key in self.data if key.startswith(prefix)]
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class TestVM(object):
|
class TestVM(object):
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
app = TestApp()
|
app = TestApp()
|
||||||
@ -269,10 +278,11 @@ class QubesVMTestsMixin(object):
|
|||||||
pass
|
pass
|
||||||
super(QubesVMTestsMixin, self).tearDown()
|
super(QubesVMTestsMixin, self).tearDown()
|
||||||
|
|
||||||
def get_vm(self, name='test', cls=qubes.vm.qubesvm.QubesVM, **kwargs):
|
def get_vm(self, name='test', cls=qubes.vm.qubesvm.QubesVM, vm=None, **kwargs):
|
||||||
vm = cls(self.app, None,
|
if not vm:
|
||||||
qid=kwargs.pop('qid', 1), name=qubes.tests.VMPREFIX + name,
|
vm = cls(self.app, None,
|
||||||
**kwargs)
|
qid=kwargs.pop('qid', 1), name=qubes.tests.VMPREFIX + name,
|
||||||
|
**kwargs)
|
||||||
self.app.domains[vm.qid] = vm
|
self.app.domains[vm.qid] = vm
|
||||||
self.app.domains[vm.uuid] = vm
|
self.app.domains[vm.uuid] = vm
|
||||||
self.app.domains[vm.name] = vm
|
self.app.domains[vm.name] = vm
|
||||||
@ -1199,6 +1209,189 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
|
|||||||
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
|
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
|
||||||
lxml.etree.XML(expected))
|
lxml.etree.XML(expected))
|
||||||
|
|
||||||
|
def test_600_libvirt_xml_hvm_cdrom_boot(self):
|
||||||
|
expected = '''<domain type="xen">
|
||||||
|
<name>test-inst-test</name>
|
||||||
|
<uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
|
||||||
|
<memory unit="MiB">400</memory>
|
||||||
|
<currentMemory unit="MiB">400</currentMemory>
|
||||||
|
<vcpu placement="static">2</vcpu>
|
||||||
|
<cpu mode='host-passthrough'>
|
||||||
|
<!-- disable nested HVM -->
|
||||||
|
<feature name='vmx' policy='disable'/>
|
||||||
|
<feature name='svm' policy='disable'/>
|
||||||
|
<!-- disable SMAP inside VM, because of Linux bug -->
|
||||||
|
<feature name='smap' policy='disable'/>
|
||||||
|
</cpu>
|
||||||
|
<os>
|
||||||
|
<type arch="x86_64" machine="xenfv">hvm</type>
|
||||||
|
<!--
|
||||||
|
For the libxl backend libvirt switches between OVMF (UEFI)
|
||||||
|
and SeaBIOS based on the loader type. This has nothing to
|
||||||
|
do with the hvmloader binary.
|
||||||
|
-->
|
||||||
|
<loader type="rom">hvmloader</loader>
|
||||||
|
<boot dev="cdrom" />
|
||||||
|
<boot dev="hd" />
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<pae/>
|
||||||
|
<acpi/>
|
||||||
|
<apic/>
|
||||||
|
<viridian/>
|
||||||
|
</features>
|
||||||
|
<clock offset="variable" adjustment="0" basis="localtime" />
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>destroy</on_reboot>
|
||||||
|
<on_crash>destroy</on_crash>
|
||||||
|
<devices>
|
||||||
|
<disk type="block" device="cdrom">
|
||||||
|
<driver name="phy" />
|
||||||
|
<source dev="/dev/sda" />
|
||||||
|
<!-- prefer xvdd for CDROM -->
|
||||||
|
<target dev="xvdd" />
|
||||||
|
<readonly/>
|
||||||
|
</disk>
|
||||||
|
<!-- server_ip is the address of stubdomain. It hosts it's own DNS server. -->
|
||||||
|
<emulator type="stubdom-linux" />
|
||||||
|
<input type="tablet" bus="usb"/>
|
||||||
|
<video>
|
||||||
|
<model type="vga"/>
|
||||||
|
</video>
|
||||||
|
<graphics type="qubes"/>
|
||||||
|
<console type="pty">
|
||||||
|
<target type="xen" port="0"/>
|
||||||
|
</console>
|
||||||
|
</devices>
|
||||||
|
</domain>
|
||||||
|
'''
|
||||||
|
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
|
||||||
|
qdb = {
|
||||||
|
'/qubes-block-devices/sda': b'',
|
||||||
|
'/qubes-block-devices/sda/desc': b'Test device',
|
||||||
|
'/qubes-block-devices/sda/size': b'1024000',
|
||||||
|
'/qubes-block-devices/sda/mode': b'r',
|
||||||
|
}
|
||||||
|
test_qdb = TestQubesDB(qdb)
|
||||||
|
dom0 = qubes.vm.adminvm.AdminVM(self.app, None)
|
||||||
|
dom0._qdb_connection = test_qdb
|
||||||
|
self.get_vm('dom0', vm=dom0)
|
||||||
|
vm = self.get_vm(uuid=my_uuid)
|
||||||
|
vm.netvm = None
|
||||||
|
vm.virt_mode = 'hvm'
|
||||||
|
vm.kernel = None
|
||||||
|
dom0.events_enabled = True
|
||||||
|
self.app.vmm.offline_mode = False
|
||||||
|
dev = qubes.devices.DeviceAssignment(
|
||||||
|
dom0, 'sda',
|
||||||
|
{'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
|
||||||
|
self.loop.run_until_complete(vm.devices['block'].attach(dev))
|
||||||
|
libvirt_xml = vm.create_config_file()
|
||||||
|
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
|
||||||
|
lxml.etree.XML(expected))
|
||||||
|
|
||||||
|
def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self):
|
||||||
|
expected = '''<domain type="xen">
|
||||||
|
<name>test-inst-test</name>
|
||||||
|
<uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
|
||||||
|
<memory unit="MiB">400</memory>
|
||||||
|
<currentMemory unit="MiB">400</currentMemory>
|
||||||
|
<vcpu placement="static">2</vcpu>
|
||||||
|
<cpu mode='host-passthrough'>
|
||||||
|
<!-- disable nested HVM -->
|
||||||
|
<feature name='vmx' policy='disable'/>
|
||||||
|
<feature name='svm' policy='disable'/>
|
||||||
|
<!-- disable SMAP inside VM, because of Linux bug -->
|
||||||
|
<feature name='smap' policy='disable'/>
|
||||||
|
</cpu>
|
||||||
|
<os>
|
||||||
|
<type arch="x86_64" machine="xenfv">hvm</type>
|
||||||
|
<!--
|
||||||
|
For the libxl backend libvirt switches between OVMF (UEFI)
|
||||||
|
and SeaBIOS based on the loader type. This has nothing to
|
||||||
|
do with the hvmloader binary.
|
||||||
|
-->
|
||||||
|
<loader type="rom">hvmloader</loader>
|
||||||
|
<boot dev="cdrom" />
|
||||||
|
<boot dev="hd" />
|
||||||
|
<cmdline>root=/dev/mapper/dmroot ro nomodeset console=hvc0 rd_NO_PLYMOUTH rd.plymouth.enable=0 plymouth.enable=0 nopat</cmdline>
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<pae/>
|
||||||
|
<acpi/>
|
||||||
|
<apic/>
|
||||||
|
<viridian/>
|
||||||
|
</features>
|
||||||
|
<clock offset="variable" adjustment="0" basis="localtime" />
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>destroy</on_reboot>
|
||||||
|
<on_crash>destroy</on_crash>
|
||||||
|
<devices>
|
||||||
|
<disk type="block" device="disk">
|
||||||
|
<driver name="phy" />
|
||||||
|
<source dev="/tmp/kernel/modules.img" />
|
||||||
|
<target dev="xvdd" />
|
||||||
|
<backenddomain name="dom0" />
|
||||||
|
</disk>
|
||||||
|
<disk type="block" device="cdrom">
|
||||||
|
<driver name="phy" />
|
||||||
|
<source dev="/dev/sda" />
|
||||||
|
<target dev="xvdi" />
|
||||||
|
<readonly/>
|
||||||
|
</disk>
|
||||||
|
<!-- server_ip is the address of stubdomain. It hosts it's own DNS server. -->
|
||||||
|
<emulator type="stubdom-linux" />
|
||||||
|
<input type="tablet" bus="usb"/>
|
||||||
|
<video>
|
||||||
|
<model type="vga"/>
|
||||||
|
</video>
|
||||||
|
<graphics type="qubes"/>
|
||||||
|
<console type="pty">
|
||||||
|
<target type="xen" port="0"/>
|
||||||
|
</console>
|
||||||
|
</devices>
|
||||||
|
</domain>
|
||||||
|
'''
|
||||||
|
qdb = {
|
||||||
|
'/qubes-block-devices/sda': b'',
|
||||||
|
'/qubes-block-devices/sda/desc': b'Test device',
|
||||||
|
'/qubes-block-devices/sda/size': b'1024000',
|
||||||
|
'/qubes-block-devices/sda/mode': b'r',
|
||||||
|
}
|
||||||
|
test_qdb = TestQubesDB(qdb)
|
||||||
|
dom0 = qubes.vm.adminvm.AdminVM(self.app, None)
|
||||||
|
dom0._qdb_connection = test_qdb
|
||||||
|
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
|
||||||
|
vm = self.get_vm(uuid=my_uuid)
|
||||||
|
vm.netvm = None
|
||||||
|
vm.virt_mode = 'hvm'
|
||||||
|
with unittest.mock.patch('qubes.config.qubes_base_dir',
|
||||||
|
'/tmp/qubes-test'):
|
||||||
|
kernel_dir = '/tmp/qubes-test/vm-kernels/dummy'
|
||||||
|
os.makedirs(kernel_dir, exist_ok=True)
|
||||||
|
open(os.path.join(kernel_dir, 'vmlinuz'), 'w').close()
|
||||||
|
open(os.path.join(kernel_dir, 'initramfs'), 'w').close()
|
||||||
|
self.addCleanup(shutil.rmtree, '/tmp/qubes-test')
|
||||||
|
vm.kernel = 'dummy'
|
||||||
|
# tests for storage are later
|
||||||
|
vm.volumes['kernel'] = unittest.mock.Mock(**{
|
||||||
|
'kernels_dir': '/tmp/kernel',
|
||||||
|
'block_device.return_value.domain': 'dom0',
|
||||||
|
'block_device.return_value.script': None,
|
||||||
|
'block_device.return_value.path': '/tmp/kernel/modules.img',
|
||||||
|
'block_device.return_value.devtype': 'disk',
|
||||||
|
'block_device.return_value.name': 'kernel',
|
||||||
|
})
|
||||||
|
dom0.events_enabled = True
|
||||||
|
self.app.vmm.offline_mode = False
|
||||||
|
dev = qubes.devices.DeviceAssignment(
|
||||||
|
dom0, 'sda',
|
||||||
|
{'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
|
||||||
|
self.loop.run_until_complete(vm.devices['block'].attach(dev))
|
||||||
|
libvirt_xml = vm.create_config_file()
|
||||||
|
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
|
||||||
|
lxml.etree.XML(expected))
|
||||||
|
|
||||||
def test_610_libvirt_xml_network(self):
|
def test_610_libvirt_xml_network(self):
|
||||||
expected = '''<domain type="xen">
|
expected = '''<domain type="xen">
|
||||||
<name>test-inst-test</name>
|
<name>test-inst-test</name>
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<source dev="{{ device.device_node }}" />
|
<source dev="{{ device.device_node }}" />
|
||||||
{%- if 'frontend-dev' in options %}
|
{%- if 'frontend-dev' in options %}
|
||||||
<target dev="{{ options.get('frontend-dev') }}" />
|
<target dev="{{ options.get('frontend-dev') }}" />
|
||||||
|
{%- elif options.get('devtype', 'disk') == 'cdrom' and not vm.kernel %}
|
||||||
|
<!-- prefer xvdd for CDROM -->
|
||||||
|
<target dev="xvdd" />
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<target dev="xvd{{dd[i]}}" />
|
<target dev="xvd{{dd[i]}}" />
|
||||||
{% set i = i + 1 %}
|
{% set i = i + 1 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user