Skip to content

Commit 1501234

Browse files
committed
Allow specifying VNC passwords for incus VMs
1 parent c079cf6 commit 1501234

File tree

4 files changed

+68
-25
lines changed

4 files changed

+68
-25
lines changed

Diff for: src/middlewared/middlewared/api/v25_04_0/virt_instance.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Annotated, Literal, TypeAlias
22

3-
from pydantic import Field, model_validator, StringConstraints
3+
from pydantic import Field, model_validator, Secret, StringConstraints
44

55
from middlewared.api.base import BaseModel, ForUpdateMetaclass, NonEmptyString, single_argument_args
66

@@ -63,6 +63,7 @@ class VirtInstanceEntry(BaseModel):
6363
raw: dict | None
6464
vnc_enabled: bool
6565
vnc_port: int | None
66+
vnc_password: Secret[NonEmptyString | None]
6667

6768

6869
# Lets require at least 32MiB of reserved memory
@@ -87,6 +88,7 @@ class VirtInstanceCreateArgs(BaseModel):
8788
memory: MemoryType | None = None
8889
enable_vnc: bool = False
8990
vnc_port: int | None = Field(ge=5900, le=65535, default=None)
91+
vnc_password: Secret[NonEmptyString | None] = None
9092

9193
@model_validator(mode='after')
9294
def validate_attrs(self):
@@ -99,6 +101,9 @@ def validate_attrs(self):
99101
if self.enable_vnc and self.vnc_port is None:
100102
raise ValueError('VNC port must be set when VNC is enabled')
101103

104+
if self.vnc_password is not None and not self.enable_vnc:
105+
raise ValueError('VNC password can only be set when VNC is enabled')
106+
102107
if self.source_type == 'ISO' and self.iso_volume is None:
103108
raise ValueError('ISO volume must be set when source type is "ISO"')
104109

@@ -119,6 +124,8 @@ class VirtInstanceUpdate(BaseModel, metaclass=ForUpdateMetaclass):
119124
memory: MemoryType | None = None
120125
vnc_port: int | None = Field(ge=5900, le=65535)
121126
enable_vnc: bool
127+
vnc_password: Secret[NonEmptyString | None]
128+
'''Setting vnc_password to null will unset VNC password'''
122129

123130

124131
class VirtInstanceUpdateArgs(BaseModel):

Diff for: src/middlewared/middlewared/plugins/virt/instance.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,35 @@ async def validate(self, new, schema_name, verrors, old=None):
158158
enable_vnc = new.get('enable_vnc')
159159
if enable_vnc is False:
160160
# User explicitly disabled VNC support, let's remove vnc port
161-
new['vnc_port'] = None
161+
new.update({
162+
'vnc_port': None,
163+
'vnc_password': None,
164+
})
165+
elif enable_vnc is True:
166+
if not old['vnc_port'] and not new.get('vnc_port'):
167+
verrors.add(f'{schema_name}.vnc_port', 'VNC port is required when VNC is enabled')
168+
elif not new.get('vnc_port'):
169+
new['vnc_port'] = old['vnc_port']
170+
171+
if 'vnc_password' not in new:
172+
new['vnc_password'] = old['vnc_password']
162173
elif enable_vnc is None:
163-
if new.get('vnc_port'):
164-
verrors.add(f'{schema_name}.enable_vnc', 'Should be set when vnc_port is specified')
165-
elif old['vnc_enabled'] and old['vnc_port']:
174+
for k in ('vnc_port', 'vnc_password'):
175+
if new.get(k):
176+
verrors.add(f'{schema_name}.enable_vnc', f'Should be set when {k!r} is specified')
177+
178+
if old['vnc_enabled'] and old['vnc_port']:
166179
# We want to handle the case where nothing has been changed on vnc attrs
167180
new.update({
168181
'enable_vnc': True,
169182
'vnc_port': old['vnc_port'],
183+
'vnc_password': old['vnc_password'],
184+
})
185+
else:
186+
new.update({
187+
'enable_vnc': False,
188+
'vnc_port': None,
189+
'vnc_password': None,
170190
})
171191
else:
172192
# Creation case
@@ -217,11 +237,20 @@ def __data_to_config(self, data: dict, raw: dict = None, instance_type=None):
217237
config['boot.autostart'] = str(data['autostart']).lower()
218238

219239
if instance_type == 'VM':
240+
config['user.ix_old_raw_qemu_config'] = raw.get('raw.qemu', '') if raw else ''
241+
config['user.ix_vnc_config'] = json.dumps({
242+
'vnc_enabled': data['enable_vnc'],
243+
'vnc_port': data['vnc_port'],
244+
'vnc_password': data['vnc_password'],
245+
})
246+
220247
if data.get('enable_vnc') and data.get('vnc_port'):
221-
config['user.ix_old_raw_qemu_config'] = raw.get('raw.qemu', '') if raw else ''
222-
config['raw.qemu'] = f'-vnc :{data["vnc_port"] - VNC_BASE_PORT}'
248+
vnc_config = f'-vnc :{data["vnc_port"] - VNC_BASE_PORT}'
249+
if data.get('vnc_password'):
250+
vnc_config = f'-object secret,id=vnc0,data={data["vnc_password"]} {vnc_config},password-secret=vnc0'
251+
252+
config['raw.qemu'] = vnc_config
223253
if data.get('enable_vnc') is False:
224-
config['user.ix_old_raw_qemu_config'] = raw.get('raw.qemu', '') if raw else ''
225254
config['raw.qemu'] = ''
226255

227256
return config

Diff for: src/middlewared/middlewared/plugins/virt/utils.py

+4-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import aiohttp
33
import enum
44
import httpx
5-
import re
5+
import json
66
from collections.abc import Callable
77

88
from .websocket import IncusWS
@@ -12,7 +12,6 @@
1212

1313
SOCKET = '/var/lib/incus/unix.socket'
1414
HTTP_URI = 'http://unix.socket'
15-
RE_VNC_PORT = re.compile(r'vnc.*?:(\d+)\s*')
1615
VNC_BASE_PORT = 5900
1716

1817

@@ -98,15 +97,9 @@ def get_vnc_info_from_config(config: dict):
9897
vnc_config = {
9998
'vnc_enabled': False,
10099
'vnc_port': None,
100+
'vnc_password': None,
101101
}
102-
if not (raw_qemu_config := config.get('raw.qemu')) or 'vnc' not in raw_qemu_config:
102+
if not (vnc_raw_config := config.get('user.ix_vnc_config')):
103103
return vnc_config
104104

105-
for flag in raw_qemu_config.split('-'):
106-
if port := RE_VNC_PORT.findall(flag):
107-
return {
108-
'vnc_enabled': True,
109-
'vnc_port': int(port[0]) + VNC_BASE_PORT,
110-
}
111-
112-
return vnc_config
105+
return json.loads(vnc_raw_config)

Diff for: tests/api2/test_virt_vm.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def vm(virt_pool):
3535
'vnc_port': VNC_PORT,
3636
'enable_vnc': True,
3737
'instance_type': 'VM',
38+
'vnc_password': 'test123'
3839
}, job=True)
3940
call('virt.instance.stop', VM_NAME, {'force': True, 'timeout': 1}, job=True)
4041
try:
@@ -119,11 +120,13 @@ def test_vm_props(vm):
119120
# Testing VNC specific bits
120121
assert instance['vnc_enabled'] is True, instance
121122
assert instance['vnc_port'] == VNC_PORT, instance
123+
assert instance['vnc_password'] == 'test123', instance
122124

123125
# Going to unset VNC
124126
call('virt.instance.update', VM_NAME, {'enable_vnc': False}, job=True)
125127
instance = call('virt.instance.get_instance', VM_NAME, {'extra': {'raw': True}})
126-
assert instance['raw']['config']['user.ix_old_raw_qemu_config'] == f'-vnc :{VNC_PORT - 5900}'
128+
assert instance['raw']['config']['user.ix_old_raw_qemu_config'] == f'-object secret,id=vnc0,data=test123 ' \
129+
f'-vnc :{VNC_PORT - 5900},password-secret=vnc0'
127130
assert instance['vnc_enabled'] is False, instance
128131
assert instance['vnc_port'] is None, instance
129132

@@ -134,9 +137,18 @@ def test_vm_props(vm):
134137
assert instance['raw']['config']['raw.qemu'] == f'-vnc :{1001}'
135138
assert instance['vnc_port'] == 6901, instance
136139

140+
# Going to update password
141+
call('virt.instance.update', VM_NAME, {'vnc_password': 'update_test123', 'enable_vnc': True}, job=True)
142+
instance = call('virt.instance.get_instance', VM_NAME, {'extra': {'raw': True}})
143+
assert instance['raw']['config'].get('user.ix_old_raw_qemu_config') == f'-vnc :{1001}'
144+
assert instance['raw']['config']['raw.qemu'] == f'-object secret,id=vnc0,data=update_test123' \
145+
f' -vnc :{1001},password-secret=vnc0'
146+
assert instance['vnc_port'] == 6901, instance
147+
137148
# Changing nothing
138149
instance = call('virt.instance.update', VM_NAME, {}, job=True)
139150
assert instance['vnc_port'] == 6901, instance
151+
assert instance['vnc_password'] == 'update_test123', instance
140152

141153

142154
def test_vm_iso_volume(vm, iso_volume):
@@ -184,18 +196,20 @@ def test_iso_param_validation_on_vm_create(virt_pool, iso_volume, error_msg):
184196
assert ve.value.errors[0].errmsg == error_msg
185197

186198

187-
@pytest.mark.parametrize('enable_vnc,vnc_port,error_msg', [
188-
(True, None, 'Value error, VNC port must be set when VNC is enabled'),
189-
(True, 6901, 'VNC port is already in use by another virt instance'),
190-
(True, 23, 'Input should be greater than or equal to 5900'),
199+
@pytest.mark.parametrize('enable_vnc,vnc_password,vnc_port,error_msg', [
200+
(True, None, None, 'Value error, VNC port must be set when VNC is enabled'),
201+
(True, None, 6901, 'VNC port is already in use by another virt instance'),
202+
(True, None, 23, 'Input should be greater than or equal to 5900'),
203+
(False, 'test_123', None, 'Value error, VNC password can only be set when VNC is enabled'),
191204
])
192-
def test_vnc_validation_on_vm_create(virt_pool, enable_vnc, vnc_port, error_msg):
205+
def test_vnc_validation_on_vm_create(virt_pool, enable_vnc, vnc_password, vnc_port, error_msg):
193206
with pytest.raises(ValidationErrors) as ve:
194207
call('virt.instance.create', {
195208
'name': 'test-vnc-vm',
196209
'instance_type': 'VM',
197210
'source_type': None,
198211
'vnc_port': vnc_port,
212+
'vnc_password': vnc_password,
199213
'enable_vnc': enable_vnc,
200214
}, job=True)
201215

0 commit comments

Comments
 (0)