Skip to content

Commit 63004fb

Browse files
committed
Add support for graphical consoles
When the ironic spec graphicalConsoles is set to "Enabled" the following occurs: - a novncproxy container is added to the conductor pod - a route is added for users to access novncproxy URLs - conductor config is modified to enable graphical console drivers - [vnc] config enables the kubernetes container_provider[1] Depends-On: openstack-k8s-operators/openstack-operator#1633 Jira: OSPRH-20211 [1] https://review.opendev.org/c/openstack/ironic/+/962242
1 parent 42c16a4 commit 63004fb

File tree

12 files changed

+206
-7
lines changed

12 files changed

+206
-7
lines changed

controllers/funcs.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ func getCommonRbacRules() []rbacv1.PolicyRule {
8686
Resources: []string{"pods"},
8787
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
8888
},
89+
{
90+
APIGroups: []string{""},
91+
Resources: []string{"secrets"},
92+
Verbs: []string{"create", "get", "list", "delete"},
93+
},
8994
}
9095
}
9196

controllers/ironic_controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,9 @@ func (r *IronicReconciler) conductorDeploymentCreateOrUpdate(
794794
TransportURLSecret: instance.Status.TransportURLSecret,
795795
KeystoneEndpoints: *keystoneEndpoints,
796796
TLS: instance.Spec.IronicAPI.TLS.Ca,
797+
GraphicalConsoles: instance.Spec.GraphicalConsoles,
798+
ConsoleImage: instance.Spec.Images.GraphicalConsole,
799+
NoVNCProxyImage: instance.Spec.Images.NoVNCProxy,
797800
}
798801

799802
if IronicConductorSpec.NodeSelector == nil {

controllers/ironicconductor_controller.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,48 @@ func (r *IronicConductorReconciler) reconcileServices(
463463
}
464464
}
465465
}
466+
if instance.Spec.GraphicalConsoles == "Enabled" {
467+
//
468+
// Create the conductor pod route to enable traffic to the
469+
// novnc service, which graphical consoles are enabled
470+
//
471+
conductorRouteLabels := map[string]string{
472+
common.AppSelector: ironic.ServiceName,
473+
common.ComponentSelector: ironic.NoVNCComponent,
474+
ironic.ConductorGroupSelector: ironicv1.ConductorGroupNull,
475+
}
476+
if instance.Spec.ConductorGroup != "" {
477+
conductorRouteLabels[ironic.ConductorGroupSelector] = strings.ToLower(instance.Spec.ConductorGroup)
478+
}
479+
480+
novncRoute := ironicconductor.RouteNoVNC(conductorPod.Name, instance, conductorRouteLabels)
481+
err = controllerutil.SetOwnerReference(&conductorPod, novncRoute, helper.GetScheme())
482+
if err != nil {
483+
return ctrl.Result{}, err
484+
}
485+
err = r.Get(
486+
ctx,
487+
types.NamespacedName{
488+
Name: novncRoute.Name,
489+
Namespace: novncRoute.Namespace,
490+
},
491+
novncRoute,
492+
)
493+
if err != nil && k8s_errors.IsNotFound(err) {
494+
Log.Info(fmt.Sprintf("Route %s does not exist, creating it", novncRoute.Name))
495+
err = r.Create(ctx, novncRoute)
496+
if err != nil {
497+
return ctrl.Result{}, err
498+
}
499+
} else {
500+
Log.Info(fmt.Sprintf("Route %s exists, updating it", novncRoute.Name))
501+
err = r.Update(ctx, novncRoute)
502+
if err != nil {
503+
return ctrl.Result{}, err
504+
}
505+
}
506+
507+
}
466508
}
467509

468510
Log.Info("Reconciled Conductor Services successfully")
@@ -922,6 +964,11 @@ func (r *IronicConductorReconciler) generateServiceConfigMaps(
922964
templateParameters["Standalone"] = instance.Spec.Standalone
923965
templateParameters["ConductorGroup"] = instance.Spec.ConductorGroup
924966
templateParameters["LogPath"] = ironicconductor.LogPath
967+
graphicalConsolesEnabled := instance.Spec.GraphicalConsoles == "Enabled"
968+
templateParameters["GraphicalConsolesEnabled"] = graphicalConsolesEnabled
969+
if graphicalConsolesEnabled {
970+
templateParameters["ConsoleImage"] = instance.Spec.ConsoleImage
971+
}
925972

926973
databaseAccount := db.GetAccount()
927974
dbSecret := db.GetSecret()
@@ -960,6 +1007,7 @@ func (r *IronicConductorReconciler) generateServiceConfigMaps(
9601007
AdditionalTemplate: map[string]string{
9611008
"ironic.conf": "/common/config/ironic.conf",
9621009
"01-conductor.conf": "/ironicconductor/config/01-conductor.conf",
1010+
"01-novnc.conf": "/ironicconductor/config/01-novnc.conf",
9631011
"03-init-container-conductor.conf": "/ironicconductor/config/03-init-container-conductor.conf",
9641012
"dnsmasq.conf": "/common/config/dnsmasq.conf",
9651013
},

pkg/ironic/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const (
4141
APIComponent = "api"
4242
// InspectorComponent -
4343
InspectorComponent = "inspector"
44+
// NoVNCComponent -
45+
NoVNCComponent = "novnc"
4446
// ConductorGroupSelector -
4547
ConductorGroupSelector = "conductorGroup"
4648
// ImageDirectory -

pkg/ironic/initcontainer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type APIDetails struct {
3636
PxeInit bool
3737
ConductorInit bool
3838
DeployHTTPURL string
39+
NoVNCProxyURL string
3940
IngressDomain string
4041
ProvisionNetwork string
4142
ImageDirectory string
@@ -57,6 +58,7 @@ func InitContainer(init APIDetails) []corev1.Container {
5758
envVars["DatabaseHost"] = env.SetValue(init.DatabaseHost)
5859
envVars["DatabaseName"] = env.SetValue(init.DatabaseName)
5960
envVars["DeployHTTPURL"] = env.SetValue(init.DeployHTTPURL)
61+
envVars["NoVNCProxyURL"] = env.SetValue(init.NoVNCProxyURL)
6062
envVars["IngressDomain"] = env.SetValue(init.IngressDomain)
6163

6264
envs := []corev1.EnvVar{

pkg/ironicconductor/service.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ func Service(
3939
ports = append(ports, httpbootPort)
4040
}
4141

42+
// Expose the ironic-novncproxy HTTP port if graphical consoles is enabled
43+
if instance.Spec.GraphicalConsoles == "Enabled" {
44+
novncPort := corev1.ServicePort{
45+
Name: ironic.NoVNCComponent,
46+
Port: 6090,
47+
Protocol: corev1.ProtocolTCP,
48+
}
49+
ports = append(ports, novncPort)
50+
51+
}
52+
4253
if len(ports) == 0 {
4354
return nil
4455
}
@@ -80,3 +91,29 @@ func Route(
8091
},
8192
}
8293
}
94+
95+
// RouteNoVNC - Route for novnc service when graphical consoles are enabled
96+
func RouteNoVNC(
97+
serviceName string,
98+
instance *ironicv1.IronicConductor,
99+
routeLabels map[string]string,
100+
) *routev1.Route {
101+
serviceRef := routev1.RouteTargetReference{
102+
Kind: "Service",
103+
Name: serviceName,
104+
}
105+
routePort := &routev1.RoutePort{
106+
TargetPort: intstr.FromString(ironic.NoVNCComponent),
107+
}
108+
return &routev1.Route{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Name: serviceName + "-novnc",
111+
Namespace: instance.Namespace,
112+
Labels: routeLabels,
113+
},
114+
Spec: routev1.RouteSpec{
115+
To: serviceRef,
116+
Port: routePort,
117+
},
118+
}
119+
}

pkg/ironicconductor/statefulset.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ func StatefulSet(
8282
PeriodSeconds: 30,
8383
InitialDelaySeconds: 5,
8484
}
85+
novncLivenessProbe := &corev1.Probe{
86+
TimeoutSeconds: 10,
87+
PeriodSeconds: 30,
88+
InitialDelaySeconds: 5,
89+
}
90+
novncReadinessProbe := &corev1.Probe{
91+
TimeoutSeconds: 10,
92+
PeriodSeconds: 30,
93+
InitialDelaySeconds: 5,
94+
}
8595

8696
args := []string{"-c", ServiceCommand}
8797

@@ -115,6 +125,12 @@ func StatefulSet(
115125
httpbootReadinessProbe.TCPSocket = &corev1.TCPSocketAction{
116126
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(8088)},
117127
}
128+
novncLivenessProbe.TCPSocket = &corev1.TCPSocketAction{
129+
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(6090)},
130+
}
131+
novncReadinessProbe.TCPSocket = &corev1.TCPSocketAction{
132+
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(6090)},
133+
}
118134

119135
// Parse the storageRequest defined in the CR
120136
storageRequest, err := resource.ParseQuantity(instance.Spec.StorageRequest)
@@ -156,13 +172,18 @@ func StatefulSet(
156172
httpbootEnvVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS")
157173
httpbootEnvVars["CONFIG_HASH"] = env.SetValue(configHash)
158174

175+
novncEnvVars := map[string]env.Setter{}
176+
novncEnvVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS")
177+
novncEnvVars["CONFIG_HASH"] = env.SetValue(configHash)
178+
159179
ramdiskLogsEnvVars := map[string]env.Setter{}
160180
ramdiskLogsEnvVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS")
161181
ramdiskLogsEnvVars["CONFIG_HASH"] = env.SetValue(configHash)
162182

163183
volumes := GetVolumes(ctx, instance)
164184
conductorVolumeMounts := GetVolumeMounts("ironic-conductor")
165185
httpbootVolumeMounts := GetVolumeMounts("httpboot")
186+
novncVolumeMounts := GetVolumeMounts("novnc")
166187
dnsmasqVolumeMounts := GetVolumeMounts("dnsmasq")
167188
ramdiskLogsVolumeMounts := GetVolumeMounts("ramdisk-logs")
168189
initVolumeMounts := GetInitVolumeMounts(instance)
@@ -175,6 +196,7 @@ func StatefulSet(
175196
dnsmasqVolumeMounts = append(dnsmasqVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...)
176197
ramdiskLogsVolumeMounts = append(ramdiskLogsVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...)
177198
initVolumeMounts = append(initVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...)
199+
novncVolumeMounts = append(novncVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...)
178200
}
179201

180202
resourceName := fmt.Sprintf("%s-%s", ironic.ServiceName, ironic.ConductorComponent)
@@ -269,11 +291,29 @@ func StatefulSet(
269291
LivenessProbe: dnsmasqLivenessProbe,
270292
// StartupProbe: startupProbe,
271293
}
272-
containers = []corev1.Container{
273-
conductorContainer,
274-
httpbootContainer,
275-
dnsmasqContainer,
294+
containers = append(containers, dnsmasqContainer)
295+
}
296+
297+
if instance.Spec.GraphicalConsoles == "Enabled" {
298+
// Only include the novnc container if graphical consoles are enabled
299+
novncContainer := corev1.Container{
300+
Name: "novnc",
301+
Command: []string{
302+
"/bin/bash",
303+
},
304+
Args: args,
305+
Image: instance.Spec.NoVNCProxyImage,
306+
SecurityContext: &corev1.SecurityContext{
307+
RunAsUser: &runAsUser,
308+
},
309+
Env: env.MergeEnvs([]corev1.EnvVar{}, novncEnvVars),
310+
VolumeMounts: novncVolumeMounts,
311+
Resources: instance.Spec.Resources,
312+
ReadinessProbe: novncReadinessProbe,
313+
LivenessProbe: novncLivenessProbe,
314+
// StartupProbe: startupProbe,
276315
}
316+
containers = append(containers, novncContainer)
277317
}
278318

279319
// Default oslo.service graceful_shutdown_timeout is 60, so align with that
@@ -346,6 +386,14 @@ func StatefulSet(
346386
// Build what the fully qualified Route hostname will be when the Route exists
347387
deployHTTPURL = "http://%(PodName)s-%(PodNamespace)s.%(IngressDomain)s/"
348388
}
389+
novncProxyURL := ""
390+
if instance.Spec.GraphicalConsoles == "Enabled" {
391+
392+
novncProtocol := "http"
393+
// TODO(stevebaker) detect if https should be used, and also for deployHTTPURL above
394+
novncDomain := "%(PodName)s-novnc-%(PodNamespace)s.%(IngressDomain)s"
395+
novncProxyURL = fmt.Sprintf("%s://%s/vnc_auto.html", novncProtocol, novncDomain)
396+
}
349397

350398
initContainerDetails := ironic.APIDetails{
351399
ContainerImage: instance.Spec.ContainerImage,
@@ -362,6 +410,7 @@ func StatefulSet(
362410
ConductorInit: true,
363411
Privileged: true,
364412
DeployHTTPURL: deployHTTPURL,
413+
NoVNCProxyURL: novncProxyURL,
365414
IngressDomain: ingressDomain,
366415
ProvisionNetwork: instance.Spec.ProvisionNetwork,
367416
}

templates/common/config/ironic.conf

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
enabled_hardware_types=ipmi,idrac,irmc,fake-hardware,redfish,manual-management,ilo,ilo5
33
enabled_bios_interfaces=no-bios,redfish,idrac-redfish,irmc,ilo
44
enabled_boot_interfaces=ipxe,ilo-ipxe,pxe,ilo-pxe,fake,redfish-virtual-media,idrac-redfish-virtual-media,ilo-virtual-media
5-
enabled_console_interfaces=ipmitool-socat,ilo,no-console,fake
65
enabled_deploy_interfaces=direct,fake,ramdisk,custom-agent
76
default_deploy_interface=direct
87
enabled_inspect_interfaces=inspector,no-inspect,irmc,fake,redfish,ilo
@@ -36,7 +35,6 @@ auth_strategy={{if .Standalone}}noauth{{else}}keystone{{end}}
3635
grub_config_path=EFI/BOOT/grub.cfg
3736
isolinux_bin=/usr/share/syslinux/isolinux.bin
3837

39-
4038
[agent]
4139
deploy_logs_local_path=/var/lib/ironic/ramdisk-logs
4240

templates/ironicconductor/bin/init.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,13 @@ if [ ! -d "/var/lib/ironic/ramdisk-logs" ]; then
8585
mkdir /var/lib/ironic/ramdisk-logs
8686
fi
8787

88+
NOVNC_PROXY_URL=$(python3 -c '
89+
import os
90+
91+
url_template = os.environ.get("NoVNCProxyURL", "")
92+
if url_template:
93+
print(url_template % os.environ)
94+
')
95+
crudini --set ${INIT_CONFIG} vnc public_url ${NOVNC_PROXY_URL}
96+
8897
echo "Conductor init successfully completed"
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
[DEFAULT]
2-
# Default conductor configuration
2+
enabled_console_interfaces={{if .GraphicalConsolesEnabled}}redfish-graphical,fake-graphical,{{end}}ipmitool-socat,ilo,no-console,fake
33

44
[conductor]
55
heartbeat_interval=20
66
heartbeat_timeout=120
77
allow_provisioning_in_maintenance=false
88
{{ if .ConductorGroup }}conductor_group={{ .ConductorGroup }}{{ end }}
9+
10+
{{if .GraphicalConsolesEnabled}}
11+
[vnc]
12+
enabled=True
13+
container_provider=kubernetes
14+
console_image={{ .ConsoleImage }}
15+
read_only=false
16+
{{end}}

0 commit comments

Comments
 (0)