Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for OpenStack hypervisor-specific VNC console access #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions env-template
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ OS_APPLICATION_CREDENTIAL_SECRET=
OS_SECGROUPS=bumblebee
OS_KEYNAME=bumblebee

# Type of desktop console server that is used by the Guacamole server:
# 'openstack_hypervisor' uses the builtin VNC server of the OpenStack hypervisor
# 'instance_builtin' uses an RDP server running within a created OpenStack desktop instance
OS_CONSOLE_SERVER=instance_builtin

### Guacamole OpenID Connect integration config

# Guacamole requires a non-confidential OIDC client with implicit flow enabled.
Expand Down
5 changes: 5 additions & 0 deletions researcher_workspace/local_settings_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
OS_NETWORK = "" # ¡Change!
OS_SECGROUPS = [] # ¡Change!

# Type of desktop console server that is used by the Guacamole server:
# 'openstack_hypervisor' uses the builtin VNC server of the OpenStack hypervisor
# 'instance_builtin' uses an RDP server running within a created OpenStack desktop instance
OS_CONSOLE_SERVER = 'instance_builtin'

### Researcher Desktop Settings

# Banner label on site only visible by users with is_superuser=True
Expand Down
5 changes: 5 additions & 0 deletions researcher_workspace/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ def get_setting(setting, default=None, required=False):

OS_PROJECT_ID = get_setting('OS_PROJECT_ID', '')

# Type of desktop console server that is used by the Guacamole server:
# 'openstack_hypervisor' uses the builtin VNC server of the OpenStack hypervisor
# 'instance_builtin' uses an RDP server running within a created OpenStack desktop instance
OS_CONSOLE_SERVER = get_setting('OS_CONSOLE_SERVER', 'instance_builtin')

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'researcher_workspace.auth.NectarAuthBackend',
Expand Down
23 changes: 23 additions & 0 deletions vm_manager/migrations/0016_add_console_addr_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2022-12-19 09:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('vm_manager', '0015_backfill_backup_expiries'),
]

operations = [
migrations.AddField(
model_name='instance',
name='console_addr',
field=models.GenericIPAddressField(blank=True, null=True),
),
migrations.AddField(
model_name='instance',
name='console_port',
field=models.PositiveIntegerField(blank=True, null=True),
),
]
46 changes: 37 additions & 9 deletions vm_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ def get_instance_by_untrusted_vm_id_2(self, vm_id, requesting_feature,
class Instance(CloudResource):
boot_volume = models.ForeignKey(Volume, on_delete=models.PROTECT, )
ip_address = models.GenericIPAddressField(null=True, blank=True)
console_addr = models.GenericIPAddressField(null=True, blank=True)
console_port = models.PositiveIntegerField(null=True, blank=True)
guac_connection = models.ForeignKey(GuacamoleConnection,
on_delete=models.SET_NULL, null=True, blank=True)
username = models.CharField(max_length=20)
Expand All @@ -335,19 +337,45 @@ def get_ip_addr(self):
self.save()
return self.ip_address

def get_console_addr_port(self):
if self.console_addr and self.console_port:
return self.console_addr, self.console_port
else:
n = get_nectar()
console_addr, console_port = n.get_console_connection(self.id)
self.console_addr = console_addr
self.console_port = console_port
self.save()
return self.console_addr, self.console_port

def get_console_protocol(self):
n = get_nectar()
return n.get_console_protocol()

def create_guac_connection(self):
# save console connection information of OpenStack instance
console_addr, console_port = self.get_console_addr_port()
console_protocol = self.get_console_protocol()

# prepare Guacamole connection parameters
params = [
('hostname', self.get_ip_addr()),
('username', self.username),
('password', self.password),
('security', 'tls'),
('ignore-cert', 'true'),
('resize-method', 'display-update'),
('enable-drive', 'true'),
('drive-path', f'/var/lib/guacd/shared-drive/{self.id}'),
('create-drive-path', 'true'),
('hostname', console_addr),
('port', console_port)
]

if console_protocol == 'rdp':
# RDP connections need additional Guacamole connection parameters
params.extend([
('username', self.username),
('password', self.password),
('security', 'tls'),
('ignore-cert', 'true'),
('resize-method', 'display-update'),
('enable-drive', 'true'),
('drive-path', f'/var/lib/guacd/shared-drive/{self.id}'),
('create-drive-path', 'true')
])

for k, v in params:
gcp, created = GuacamoleConnectionParameter.objects.get_or_create(
connection=self.guac_connection,
Expand Down
9 changes: 8 additions & 1 deletion vm_manager/tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid

from vm_manager.tests.common import UUID_1, UUID_2
from vm_manager.utils.utils import Nectar


class Fake(object):
Expand Down Expand Up @@ -40,7 +41,7 @@ class FakeServer(Fake):
]


class FakeNectar(object):
class FakeNectar(Nectar):
def __init__(self):
self.nova = Mock()
self.nova.flavors.list = Mock(return_value=FLAVORS)
Expand All @@ -55,3 +56,9 @@ def __init__(self):
self.cinder.volumes.list = Mock(return_value=VOLUMES)
self.cinder.volumes.create = Mock(
return_value=FakeVolume(id=UUID_1))

def get_console_connection(self, server_id):
pass

def get_console_protocol(self):
pass
29 changes: 17 additions & 12 deletions vm_manager/tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
ResizeFactory, VMStatusFactory
from vm_manager.tests.fakes import Fake, FakeNectar
from vm_manager.constants import ERROR, VM_DELETED
from vm_manager.utils.utils import get_nectar
from vm_manager.utils.utils import get_nectar, NectarFactory

from vm_manager.models import Instance, Volume, Resize, VMStatus, \
_create_hostname_id
Expand Down Expand Up @@ -61,9 +61,9 @@ def test_superclass_methods(self):
fake_volume = self.make_volume()
self.do_superclass_method_tests(fake_volume)

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
@patch('vm_manager.models._create_hostname_id')
def test_volume_save(self, mock_gen):
def test_volume_save(self, mock_gen, mock_cn):
mock_gen.return_value = "fnord"
id = uuid.uuid4()
volume = self.make_volume(id=id)
Expand Down Expand Up @@ -135,8 +135,8 @@ def test_superclass_methods(self):

self.do_superclass_method_tests(fake_instance)

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
def test_get_ip_addr(self):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_get_ip_addr(self, mock_cn):
fake_volume = self.make_volume()
fake_instance = InstanceFactory.create(
id=uuid.uuid4(), user=self.user, boot_volume=fake_volume)
Expand Down Expand Up @@ -165,8 +165,8 @@ def test_get_ip_addr(self):
self.assertEqual(dummy_ip, ip)
self.assertIsNotNone(fake_instance.ip_address)

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
def test_get_status(self):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_get_status(self, mock_cn):
fake = get_nectar()
fake.nova.servers.get.return_value = Fake(status='testing')
fake.nova.servers.get.reset_mock()
Expand All @@ -180,13 +180,18 @@ def test_get_status(self):
fake.nova.servers.get.assert_called_once_with(fake_instance.id)
self.assertEqual('testing', status)

def test_create_guac_connection(self):
@patch.object(FakeNectar, 'get_console_protocol', return_value="rdp")
@patch.object(FakeNectar, 'get_console_connection',
return_value=("192.168.122.20", 5901))
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_create_guac_connection(self, mock_cn, mock_conn, mock_proto):
fake_volume = self.make_volume()
fake_guac_connection = GuacamoleConnectionFactory.create()
fake_instance = InstanceFactory.create(
id=uuid.uuid4(), user=self.user, boot_volume=fake_volume,
guac_connection=fake_guac_connection,
ip_address="10.0.0.1")
ip_address="10.0.0.1", console_addr="192.168.122.20",
console_port=5901)

with self.assertRaises(GuacamoleEntity.DoesNotExist):
self.assertIsNone(GuacamoleEntity.objects.get(
Expand All @@ -200,7 +205,7 @@ def test_create_guac_connection(self):

entity = GuacamoleEntity.objects.get(name=self.user.username)
self.assertIsNotNone(entity)
self.assertEqual(9,
self.assertEqual(10,
GuacamoleConnectionParameter.objects.filter(
connection=fake_guac_connection).count())
self.assertEqual(1,
Expand Down Expand Up @@ -265,8 +270,8 @@ def test_get_instance_by_ip(self):
f"with ip_address={ip_address}",
str(cm.exception))

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
def test_get_instance_by_ip_with_lookup(self):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_get_instance_by_ip_with_lookup(self, mock_cn):
ip_address = '10.0.0.3'
ip_address_2 = '10.0.0.4'

Expand Down
18 changes: 9 additions & 9 deletions vm_manager/tests/unit/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from vm_manager.models import VMStatus, Instance, Volume, Resize, Expiration, \
EXP_INITIAL, EXP_FIRST_WARNING, EXP_EXPIRING, EXP_EXPIRY_COMPLETED, \
EXP_EXPIRY_FAILED_RETRYABLE
from vm_manager.utils.utils import get_nectar, after_time
from vm_manager.utils.utils import get_nectar, after_time, NectarFactory
from vm_manager.views import launch_vm_worker, delete_vm_worker, \
shelve_vm_worker, unshelve_vm_worker, reboot_vm_worker, \
supersize_vm_worker, downsize_vm_worker
Expand Down Expand Up @@ -408,8 +408,8 @@ def test_downsize_vm(self, mock_rq):
self.assertEqual(0, vm_status.status_progress)
self.assertTrue(now < vm_status.wait_time)

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
def test_get_vm_state(self):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_get_vm_state(self, mock_cn):
self.build_existing_vm(None)
self.assertEqual((NO_VM, "No VM", None),
get_vm_state(self.vm_status,
Expand Down Expand Up @@ -486,10 +486,10 @@ def test_get_vm_state(self):
get_vm_state(self.vm_status,
self.user, self.UBUNTU))

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
@patch('vm_manager.models.Instance.get_url')
@patch('vm_manager.views.InstanceExpiryPolicy')
def test_get_vm_state_2(self, mock_policy_class, mock_get_url):
def test_get_vm_state_2(self, mock_policy_class, mock_get_url, mock_cn):
url = "https://foo/bar"
mock_get_url.return_value = url

Expand Down Expand Up @@ -530,10 +530,10 @@ def test_get_vm_state_2(self, mock_policy_class, mock_get_url):
instance.error_message)
self.assertIsNone(instance.boot_volume.error_message)

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
@patch('vm_manager.models.Instance.get_url')
@patch('vm_manager.views.BoostExpiryPolicy')
def test_get_vm_state_3(self, mock_policy_class, mock_get_url):
def test_get_vm_state_3(self, mock_policy_class, mock_get_url, mock_cn):
url = "https://foo/bar"
mock_get_url.return_value = url

Expand Down Expand Up @@ -566,9 +566,9 @@ def test_get_vm_state_3(self, mock_policy_class, mock_get_url):
self.instance.id),
get_vm_state(self.vm_status, self.user, self.UBUNTU))

@patch('vm_manager.utils.utils.Nectar', new=FakeNectar)
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
@patch('vm_manager.views.VolumeExpiryPolicy')
def test_get_vm_state_4(self, mock_policy_class):
def test_get_vm_state_4(self, mock_policy_class, mock_cn):

# Not testing the expiration policy decisions
mock_policy = Mock()
Expand Down
22 changes: 16 additions & 6 deletions vm_manager/tests/unit/vm_functions/test_admin_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

from django.utils.timezone import utc

from vm_manager.tests.fakes import FakeNectar
from vm_manager.tests.unit.vm_functions.base import VMFunctionTestBase

from vm_manager.constants import VM_OKAY, VM_DELETED, VM_WAITING, VM_SUPERSIZED
from vm_manager.models import VMStatus, Volume, Instance
from vm_manager.tests.factories import ResizeFactory
from vm_manager.utils.utils import NectarFactory
from vm_manager.vm_functions.admin_functionality import \
admin_shelve_instance, admin_delete_instance_and_volume, \
admin_delete_volume, admin_archive_volume, \
Expand Down Expand Up @@ -67,7 +69,9 @@ def test_admin_delete_instance_and_volume_2(self, mock_delete, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.archive_volume_worker')
def test_admin_archive_instance_and_volume(self, mock_archive, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_archive_instance_and_volume(self, mock_cn, mock_archive,
mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand All @@ -90,7 +94,9 @@ def test_admin_archive_instance_and_volume(self, mock_archive, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.archive_volume_worker')
def test_admin_archive_instance_and_volume_2(self, mock_archive, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_archive_instance_and_volume_2(self, mock_cn, mock_archive,
mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand All @@ -112,7 +118,8 @@ def test_admin_archive_instance_and_volume_2(self, mock_archive, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.delete_volume')
def test_admin_delete_volume(self, mock_delete, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_delete_volume(self, mock_cn, mock_delete, mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand All @@ -132,7 +139,8 @@ def test_admin_delete_volume(self, mock_delete, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.archive_volume_worker')
def test_admin_archive_volume(self, mock_archive, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_archive_volume(self, mock_cn, mock_archive, mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand All @@ -151,7 +159,8 @@ def test_admin_archive_volume(self, mock_archive, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.logger')
def test_admin_shelve_instance(self, mock_logger, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_shelve_instance(self, mock_cn, mock_logger, mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand All @@ -174,7 +183,8 @@ def test_admin_shelve_instance(self, mock_logger, mock_rq):

@patch('vm_manager.vm_functions.admin_functionality.django_rq')
@patch('vm_manager.vm_functions.admin_functionality.logger')
def test_admin_downsize_instance(self, mock_logger, mock_rq):
@patch.object(NectarFactory, 'create', return_value=FakeNectar())
def test_admin_downsize_instance(self, mock_cn, mock_logger, mock_rq):
mock_queue = Mock()
mock_rq.get_queue.return_value = mock_queue
fake_volume, fake_instance, fake_vmstatus = \
Expand Down
Loading