diff --git a/deploy/04ui.yaml b/deploy/04ui.yaml index f0a62cba1..74fb412e6 100644 --- a/deploy/04ui.yaml +++ b/deploy/04ui.yaml @@ -20,6 +20,9 @@ spec: imagePullPolicy: IfNotPresent ports: - containerPort: 80 + envFrom: + - configMapRef: + name: pf9-env volumeMounts: - name: pf9-logs mountPath: /var/log/pf9 diff --git a/deploy/05controller-deployment.yaml b/deploy/05controller-deployment.yaml index 5c3bc9dbc..501edfa52 100644 --- a/deploy/05controller-deployment.yaml +++ b/deploy/05controller-deployment.yaml @@ -41,6 +41,7 @@ spec: envFrom: - configMapRef: name: pf9-env + optional: true image: quay.io/platform9/vjailbreak-controller:main imagePullPolicy: IfNotPresent lifecycle: diff --git a/deploy/06vpwned-deployment.yaml b/deploy/06vpwned-deployment.yaml index 0e8c38a15..480b2b49d 100644 --- a/deploy/06vpwned-deployment.yaml +++ b/deploy/06vpwned-deployment.yaml @@ -27,6 +27,7 @@ spec: - envFrom: - configMapRef: name: pf9-env + optional: true image: quay.io/platform9/vjailbreak-vpwned:main imagePullPolicy: IfNotPresent name: vpwned @@ -34,6 +35,8 @@ spec: - containerPort: 3001 protocol: TCP resources: {} + securityContext: + privileged: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -47,6 +50,7 @@ spec: - name: ndots value: "1" dnsPolicy: ClusterFirst + hostPID: true restartPolicy: Always schedulerName: default-scheduler securityContext: {} diff --git a/deploy/07ui-deployment.yaml b/deploy/07ui-deployment.yaml index a1ba9b976..545a90efb 100644 --- a/deploy/07ui-deployment.yaml +++ b/deploy/07ui-deployment.yaml @@ -28,6 +28,10 @@ spec: imagePullPolicy: IfNotPresent ports: - containerPort: 80 + envFrom: + - configMapRef: + name: pf9-env + optional: true volumeMounts: - name: nginx-shadow-htpasswd mountPath: /etc/nginx/shadow diff --git a/deploy/installer.yaml b/deploy/installer.yaml index a65a04f0b..9dd72b215 100644 --- a/deploy/installer.yaml +++ b/deploy/installer.yaml @@ -4454,6 +4454,7 @@ spec: envFrom: - configMapRef: name: pf9-env + optional: true image: quay.io/platform9/vjailbreak-controller:main imagePullPolicy: IfNotPresent lifecycle: @@ -4541,6 +4542,7 @@ spec: - envFrom: - configMapRef: name: pf9-env + optional: true image: quay.io/platform9/vjailbreak-vpwned:main imagePullPolicy: IfNotPresent name: vpwned @@ -4548,6 +4550,8 @@ spec: - containerPort: 3001 protocol: TCP resources: {} + securityContext: + privileged: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -4561,6 +4565,7 @@ spec: - name: ndots value: "1" dnsPolicy: ClusterFirst + hostPID: true restartPolicy: Always schedulerName: default-scheduler securityContext: {} @@ -4634,6 +4639,10 @@ spec: imagePullPolicy: IfNotPresent ports: - containerPort: 80 + envFrom: + - configMapRef: + name: pf9-env + optional: true volumeMounts: - name: nginx-shadow-htpasswd mountPath: /etc/nginx/shadow diff --git a/image_builder/configs/env b/image_builder/configs/env index 6e53f8049..030a6fb48 100644 --- a/image_builder/configs/env +++ b/image_builder/configs/env @@ -1,4 +1,5 @@ # This file is used to set environment variables to be injected into v2v-helper # User can populate this file via cloud-init +TZ=UTC diff --git a/image_builder/configs/vjailbreak-settings.yaml b/image_builder/configs/vjailbreak-settings.yaml index a0b962bfc..a993979cb 100644 --- a/image_builder/configs/vjailbreak-settings.yaml +++ b/image_builder/configs/vjailbreak-settings.yaml @@ -20,6 +20,7 @@ data: VMWARE_CREDS_REQUEUE_AFTER_MINUTES: "60" # number of minutes to requeue after for vmware creds VALIDATE_RDM_OWNER_VMS: "true" # validate RDM owner VMs before migration DEPLOYMENT_NAME: vJailbreak + TIMEZONE: "" PERIODIC_SYNC_MAX_RETRIES: "3" # max number of retries for CBT sync PERIODIC_SYNC_RETRY_CAP: "3h" # max retry interval for CBT sync AUTO_FSTAB_UPDATE: "true" # automatically update fstab @@ -30,4 +31,4 @@ data: V2V_HELPER_POD_MEMORY_LIMIT: "5Gi" V2V_HELPER_POD_EPHEMERAL_STORAGE_REQUEST: "3Gi" V2V_HELPER_POD_EPHEMERAL_STORAGE_LIMIT: "3Gi" - + NTP_SERVERS: "" diff --git a/image_builder/cronjob/version-checker.yaml b/image_builder/cronjob/version-checker.yaml index f760ea4b9..9f034d9c2 100644 --- a/image_builder/cronjob/version-checker.yaml +++ b/image_builder/cronjob/version-checker.yaml @@ -47,6 +47,10 @@ spec: containers: - name: version-checker image: quay.io/platform9/vjailbreak:alpine + envFrom: + - configMapRef: + name: pf9-env + optional: true command: - /bin/sh - -c diff --git a/image_builder/scripts/install.sh b/image_builder/scripts/install.sh index 1ad1ebb76..56b480090 100644 --- a/image_builder/scripts/install.sh +++ b/image_builder/scripts/install.sh @@ -105,6 +105,234 @@ set_default_password() { set_default_password check_command "Setting default password for ubuntu user" +install_time_settings_apply_script() { + log "Installing vJailbreak time settings apply script (NTP/timezone)..." + + sudo mkdir -p /etc/pf9 + + sudo tee /etc/pf9/apply-time-settings.sh > /dev/null <<'EOF' +#!/bin/bash +set -euo pipefail + +LOG_DIR="/var/log/pf9" +STATE_DIR="/var/lib/pf9" +LOG_FILE="${LOG_DIR}/time-settings.log" +STATE_FILE="${STATE_DIR}/time-settings.state" +TIMESYNCD_CONF_DIR="/etc/systemd/timesyncd.conf.d" +TIMESYNCD_CONF_FILE="${TIMESYNCD_CONF_DIR}/99-vjailbreak.conf" + +mkdir -p "$LOG_DIR" "$STATE_DIR" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +normalize_servers() { + printf '%s' "${1:-}" | tr ',\n' ' ' | xargs || true +} + +is_valid_ntp_server() { + local server="$1" + + [ -n "$server" ] || return 1 + + if [[ "$server" == *"://"* ]] || [[ "$server" == */* ]]; then + return 1 + fi + + if [[ "$server" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + local o1 o2 o3 o4 + IFS='.' read -r o1 o2 o3 o4 <<< "$server" + for octet in "$o1" "$o2" "$o3" "$o4"; do + if [ "$octet" -lt 0 ] || [ "$octet" -gt 255 ]; then + return 1 + fi + done + return 0 + fi + + if [[ "$server" =~ ^[a-zA-Z0-9.-]+$ ]] && [[ "$server" != .* ]] && [[ "$server" != *..* ]]; then + IFS='.' read -r -a labels <<< "$server" + for label in "${labels[@]}"; do + if [ -z "$label" ] || [ "${#label}" -gt 63 ] || [[ ! "$label" =~ ^[a-zA-Z0-9-]+$ ]] || [[ "$label" == -* ]] || [[ "$label" == *- ]]; then + return 1 + fi + done + return 0 + fi + + return 1 +} + +filter_valid_ntp_servers() { + local raw="$1" + local valid="" + local invalid="" + local server + + for server in $raw; do + if is_valid_ntp_server "$server"; then + valid+=" $server" + else + invalid+=" $server" + fi + done + + valid="$(echo "$valid" | xargs || true)" + invalid="$(echo "$invalid" | xargs || true)" + + if [ -n "$invalid" ]; then + log "Ignoring invalid NTP server entries: $invalid" + fi + + printf '%s' "$valid" +} + +write_timesyncd_conf() { + local servers="$1" + mkdir -p "$TIMESYNCD_CONF_DIR" + cat </dev/null +[Time] +NTP=${servers} +CONF +} + +clear_timesyncd_conf() { + rm -f "$TIMESYNCD_CONF_FILE" +} + +update_pf9_env_timezone() { + local tz="$1" + if [ -z "$tz" ]; then + return 0 + fi + + if [ -f /etc/pf9/env ]; then + if grep -q '^TZ=' /etc/pf9/env; then + sudo sed -i "s#^TZ=.*#TZ=${tz}#" /etc/pf9/env || true + else + printf '\nTZ=%s\n' "$tz" | sudo tee -a /etc/pf9/env >/dev/null + fi + fi + + if kubectl -n migration-system get configmap pf9-env >/dev/null 2>&1; then + kubectl -n migration-system patch configmap pf9-env --type merge -p "{\"data\":{\"TZ\":\"${tz}\"}}" >/dev/null 2>&1 || true + for deployment in migration-controller-manager migration-vpwned-sdk vjailbreak-ui; do + kubectl -n migration-system rollout restart deployment "$deployment" >/dev/null 2>&1 || true + done + fi +} + +if [ -f "/etc/pf9/k3s.env" ]; then + source "/etc/pf9/k3s.env" || true +fi + +if [ "${IS_MASTER:-}" != "true" ]; then + exit 0 +fi + +if ! command -v kubectl >/dev/null 2>&1; then + log "kubectl not found yet; time settings will be applied by watcher when ready" + exit 0 +fi + +if ! kubectl -n migration-system get configmap vjailbreak-settings >/dev/null 2>&1; then + log "vjailbreak-settings ConfigMap not available yet; watcher will handle it" + exit 0 +fi + +get_cm_val() { + local key="$1" + kubectl -n migration-system get configmap vjailbreak-settings -o jsonpath="{.data.${key}}" 2>/dev/null || true +} + +timezone="$(get_cm_val TIMEZONE)" +ntp_servers_raw="$(get_cm_val NTP_SERVERS)" + +timezone="$(echo "${timezone:-}" | xargs || true)" +ntp_servers="$(filter_valid_ntp_servers "$(normalize_servers "${ntp_servers_raw:-}")")" + +desired_fingerprint="$(printf '%s\n%s\n' "${timezone}" "${ntp_servers}" | sha256sum | awk '{print $1}')" +current_fingerprint="" +if [ -f "$STATE_FILE" ]; then + current_fingerprint="$(cat "$STATE_FILE" 2>/dev/null || true)" +fi + +if [ "$desired_fingerprint" = "$current_fingerprint" ]; then + exit 0 +fi + +sync_enabled="false" +target_timezone="" + +if [ -n "$ntp_servers" ]; then + sync_enabled="true" + write_timesyncd_conf "$ntp_servers" + if [ -n "$timezone" ] && [ -f "/usr/share/zoneinfo/${timezone}" ]; then + target_timezone="$timezone" + else + target_timezone="UTC" + log "No timezone configured with custom NTP servers; defaulting timezone to UTC" + fi +elif [ -n "$timezone" ] && [ -f "/usr/share/zoneinfo/${timezone}" ]; then + sync_enabled="true" + target_timezone="$timezone" + clear_timesyncd_conf +else + clear_timesyncd_conf + target_timezone="UTC" +fi + +if [ -n "$ntp_servers" ]; then + log "Applying time settings: TIMEZONE=${target_timezone} NTP_SERVERS=${ntp_servers}" +elif [ -n "$timezone" ]; then + log "Applying time settings: TIMEZONE=${target_timezone} NTP_SERVERS=" +else + log "Applying time settings: no timezone or NTP configured; disabling NTP sync, resetting to UTC" +fi + +if [ -n "$target_timezone" ]; then + current_tz="$(timedatectl show -p Timezone --value 2>/dev/null || true)" + if [ "$current_tz" != "$target_timezone" ]; then + if timedatectl set-timezone "$target_timezone"; then + log "Timezone updated to ${target_timezone}" + else + log "Failed to set timezone to ${target_timezone}" + fi + fi +fi + +update_pf9_env_timezone "$target_timezone" + +if [ "$sync_enabled" = "true" ]; then + timedatectl set-ntp true >/dev/null 2>&1 || true + systemctl enable --now systemd-timesyncd >/dev/null 2>&1 || true + systemctl restart systemd-timesyncd >/dev/null 2>&1 || true +else + timedatectl set-ntp false >/dev/null 2>&1 || true + systemctl disable --now systemd-timesyncd >/dev/null 2>&1 || true +fi + +echo "$desired_fingerprint" > "$STATE_FILE" +log "Time settings applied" +EOF + + sudo chmod +x /etc/pf9/apply-time-settings.sh + + sudo rm -f /etc/pf9/watch-time-settings.sh + sudo rm -f /etc/logrotate.d/pf9-time-settings + sudo rm -f /etc/systemd/system/vjailbreak-time-settings-watcher.service + sudo rm -f /etc/systemd/system/vjailbreak-time-settings.timer + sudo rm -f /etc/systemd/system/vjailbreak-time-settings.service + sudo systemctl daemon-reload + sudo systemctl disable --now vjailbreak-time-settings-watcher.service >/dev/null 2>&1 || true + sudo systemctl disable --now vjailbreak-time-settings.timer >/dev/null 2>&1 || true + sudo systemctl disable --now vjailbreak-time-settings.service >/dev/null 2>&1 || true + log "Time settings apply script installed. Watcher service removed." +} + +install_time_settings_apply_script + # Create /etc/htpasswd with ubuntu user using openssl apr1 hash (airgapped-safe) sudo sh -c 'umask 0177; mkdir -p /etc; echo "admin:$(openssl passwd -apr1 password)" > /etc/htpasswd' sudo chmod 644 /etc/htpasswd @@ -226,7 +454,7 @@ if [ "$IS_MASTER" == "true" ]; then log "Rsync daemon started successfully." # Create a config map from env file. - kubectl create configmap pf9-env -n migration-system --from-file=/etc/pf9/env + kubectl create configmap pf9-env -n migration-system --from-env-file=/etc/pf9/env check_command "Creating config map from env file" log "Config map created successfully." @@ -245,6 +473,8 @@ if [ "$IS_MASTER" == "true" ]; then log "WARNING: /etc/pf9/yamls/cert-manager not found. Skipping cert-manager installation." fi + install_time_settings_apply_script + else log "Setting up K3s Worker..." diff --git a/k8s/migration/config/addons/k8s.svc.yaml b/k8s/migration/config/addons/k8s.svc.yaml index 681107aea..a1b3dc5ab 100644 --- a/k8s/migration/config/addons/k8s.svc.yaml +++ b/k8s/migration/config/addons/k8s.svc.yaml @@ -29,6 +29,8 @@ spec: - containerPort: 3001 protocol: TCP resources: {} + securityContext: + privileged: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -40,6 +42,7 @@ spec: envFrom: - configMapRef: name: pf9-env + optional: true dnsConfig: options: - name: ndots @@ -47,6 +50,7 @@ spec: dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler + hostPID: true securityContext: {} serviceAccountName: migration-controller-manager terminationGracePeriodSeconds: 30 diff --git a/k8s/migration/config/manager/manager.yaml b/k8s/migration/config/manager/manager.yaml index 85cc3873c..689249f75 100644 --- a/k8s/migration/config/manager/manager.yaml +++ b/k8s/migration/config/manager/manager.yaml @@ -70,6 +70,7 @@ spec: envFrom: - configMapRef: name: pf9-env + optional: true args: - --leader-elect=false - --health-probe-bind-address=:8081 diff --git a/pkg/common/config/settings.go b/pkg/common/config/settings.go index e8b5624c6..ae5670261 100644 --- a/pkg/common/config/settings.go +++ b/pkg/common/config/settings.go @@ -39,6 +39,8 @@ type VjailbreakSettings struct { V2VHelperPodMemoryLimit string V2VHelperPodEphemeralStorageRequest string V2VHelperPodEphemeralStorageLimit string + Timezone string + NTPServers string } // Atoi is a helper function to convert string to int with a default value of 0 @@ -85,6 +87,8 @@ func GetVjailbreakSettings(ctx context.Context, k8sClient client.Client) (*Vjail V2VHelperPodMemoryLimit: constants.V2VHelperPodMemoryLimit, V2VHelperPodEphemeralStorageRequest: constants.V2VHelperPodEphemeralStorageRequest, V2VHelperPodEphemeralStorageLimit: constants.V2VHelperPodEphemeralStorageLimit, + Timezone: "", + NTPServers: "", }, nil } @@ -183,6 +187,13 @@ func GetVjailbreakSettings(ctx context.Context, k8sClient client.Client) (*Vjail vjailbreakSettingsCM.Data[constants.V2VHelperPodEphemeralStorageLimitKey] = constants.V2VHelperPodEphemeralStorageLimit } + if vjailbreakSettingsCM.Data["TIMEZONE"] == "" { + vjailbreakSettingsCM.Data["TIMEZONE"] = "" + } + if vjailbreakSettingsCM.Data["NTP_SERVERS"] == "" { + vjailbreakSettingsCM.Data["NTP_SERVERS"] = "" + } + return &VjailbreakSettings{ ChangedBlocksCopyIterationThreshold: Atoi(vjailbreakSettingsCM.Data["CHANGED_BLOCKS_COPY_ITERATION_THRESHOLD"]), PeriodicSyncInterval: vjailbreakSettingsCM.Data["PERIODIC_SYNC_INTERVAL"], @@ -209,5 +220,7 @@ func GetVjailbreakSettings(ctx context.Context, k8sClient client.Client) (*Vjail V2VHelperPodMemoryLimit: vjailbreakSettingsCM.Data[constants.V2VHelperPodMemoryLimitKey], V2VHelperPodEphemeralStorageRequest: vjailbreakSettingsCM.Data[constants.V2VHelperPodEphemeralStorageRequestKey], V2VHelperPodEphemeralStorageLimit: vjailbreakSettingsCM.Data[constants.V2VHelperPodEphemeralStorageLimitKey], + Timezone: strings.TrimSpace(vjailbreakSettingsCM.Data["TIMEZONE"]), + NTPServers: strings.TrimSpace(vjailbreakSettingsCM.Data["NTP_SERVERS"]), }, nil } diff --git a/pkg/vpwned/api/proto/v1/service/api.pb.go b/pkg/vpwned/api/proto/v1/service/api.pb.go index 8f0b59453..69b9339fc 100644 --- a/pkg/vpwned/api/proto/v1/service/api.pb.go +++ b/pkg/vpwned/api/proto/v1/service/api.pb.go @@ -5099,6 +5099,86 @@ func (x *ResolveCinderVolumeResponse) GetVolume() *VolumeInfo { return nil } +type ApplyTimeSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyTimeSettingsRequest) Reset() { + *x = ApplyTimeSettingsRequest{} + mi := &file_sdk_proto_v1_api_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyTimeSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyTimeSettingsRequest) ProtoMessage() {} + +func (x *ApplyTimeSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_sdk_proto_v1_api_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyTimeSettingsRequest.ProtoReflect.Descriptor instead. +func (*ApplyTimeSettingsRequest) Descriptor() ([]byte, []int) { + return file_sdk_proto_v1_api_proto_rawDescGZIP(), []int{80} +} + +type ApplyTimeSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyTimeSettingsResponse) Reset() { + *x = ApplyTimeSettingsResponse{} + mi := &file_sdk_proto_v1_api_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyTimeSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyTimeSettingsResponse) ProtoMessage() {} + +func (x *ApplyTimeSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_sdk_proto_v1_api_proto_msgTypes[81] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyTimeSettingsResponse.ProtoReflect.Descriptor instead. +func (*ApplyTimeSettingsResponse) Descriptor() ([]byte, []int) { + return file_sdk_proto_v1_api_proto_rawDescGZIP(), []int{81} +} + +func (x *ApplyTimeSettingsResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + var File_sdk_proto_v1_api_proto protoreflect.FileDescriptor const file_sdk_proto_v1_api_proto_rawDesc = "" + @@ -5483,7 +5563,10 @@ const file_sdk_proto_v1_api_proto_rawDesc = "" + "\x1bResolveCinderVolumeResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12'\n" + - "\x06volume\x18\x03 \x01(\v2\x0f.api.VolumeInfoR\x06volume*j\n" + + "\x06volume\x18\x03 \x01(\v2\x0f.api.VolumeInfoR\x06volume\"\x1a\n" + + "\x18ApplyTimeSettingsRequest\"5\n" + + "\x19ApplyTimeSettingsResponse\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage*j\n" + "\vPowerStatus\x12\x0f\n" + "\vPOWERED_OFF\x10\x00\x12\x0e\n" + "\n" + @@ -5521,11 +5604,12 @@ const file_sdk_proto_v1_api_proto_rawDesc = "" + "\x06WhoAmI\x12\x12.api.WhoAmIRequest\x1a\x13.api.WhoAmIResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/vpw/v1/who_am_i\x12k\n" + "\x0eListBootSource\x12\x1a.api.ListBootSourceRequest\x1a\x1b.api.ListBootSourceResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/vpw/v1/list_boot_source\x12b\n" + "\rReclaimBMHost\x12\x15.api.ReclaimBMRequest\x1a\x16.api.ReclaimBMResponse\"\"\x82\xd3\xe4\x93\x02\x1c:\x01*\"\x17/vpw/v1/reclaim_bm_host\x12i\n" + - "\rDeployMachine\x12\x19.api.DeployMachineRequest\x1a\x1a.api.DeployMachineResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/vpw/v1/deploy_machine2\xd7\x04\n" + + "\rDeployMachine\x12\x19.api.DeployMachineRequest\x1a\x1a.api.DeployMachineResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/vpw/v1/deploy_machine2\xdd\x05\n" + "\x0eVailbreakProxy\x12\x82\x01\n" + "\x13ValidateOpenstackIp\x12\x1f.api.ValidateOpenstackIpRequest\x1a .api.ValidateOpenstackIpResponse\"(\x82\xd3\xe4\x93\x02\":\x01*\"\x1d/vpw/v1/validate_openstack_ip\x12\x89\x01\n" + "\x15RevalidateCredentials\x12!.api.RevalidateCredentialsRequest\x1a\".api.RevalidateCredentialsResponse\")\x82\xd3\xe4\x93\x02#:\x01*\"\x1e/vpw/v1/revalidate_credentials\x12~\n" + - "\x12InjectEnvVariables\x12\x1e.api.InjectEnvVariablesRequest\x1a\x1f.api.InjectEnvVariablesResponse\"'\x82\xd3\xe4\x93\x02!:\x01*\"\x1c/vpw/v1/inject_env_variables\x12\xb3\x01\n" + + "\x12InjectEnvVariables\x12\x1e.api.InjectEnvVariablesRequest\x1a\x1f.api.InjectEnvVariablesResponse\"'\x82\xd3\xe4\x93\x02!:\x01*\"\x1c/vpw/v1/inject_env_variables\x12\x83\x01\n" + + "\x11ApplyTimeSettings\x12\x1d.api.ApplyTimeSettingsRequest\x1a\x1e.api.ApplyTimeSettingsResponse\"/\x82\xd3\xe4\x93\x02):\x01*\"$/vpw/v1/settings/apply-time-settings\x12\xb3\x01\n" + "\x1fCheckNetworkSubnetCompatibility\x12+.api.CheckNetworkSubnetCompatibilityRequest\x1a,.api.CheckNetworkSubnetCompatibilityResponse\"5\x82\xd3\xe4\x93\x02/:\x01*\"*/vpw/v1/check_network_subnet_compatibility2\xfd\x05\n" + "\fStorageArray\x12\x7f\n" + "\x13ValidateCredentials\x12 .api.ValidateStorageCredsRequest\x1a!.api.ValidateStorageCredsResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/vpw/v1/storage/validate\x12\x8f\x01\n" + @@ -5550,7 +5634,7 @@ func file_sdk_proto_v1_api_proto_rawDescGZIP() []byte { } var file_sdk_proto_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_sdk_proto_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 80) +var file_sdk_proto_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 82) var file_sdk_proto_v1_api_proto_goTypes = []any{ (PowerStatus)(0), // 0: api.PowerStatus (BootDevice)(0), // 1: api.BootDevice @@ -5634,6 +5718,8 @@ var file_sdk_proto_v1_api_proto_goTypes = []any{ (*GetMappedGroupsResponse)(nil), // 79: api.GetMappedGroupsResponse (*ResolveCinderVolumeRequest)(nil), // 80: api.ResolveCinderVolumeRequest (*ResolveCinderVolumeResponse)(nil), // 81: api.ResolveCinderVolumeResponse + (*ApplyTimeSettingsRequest)(nil), // 82: api.ApplyTimeSettingsRequest + (*ApplyTimeSettingsResponse)(nil), // 83: api.ApplyTimeSettingsResponse } var file_sdk_proto_v1_api_proto_depIdxs = []int32{ 5, // 0: api.AvailableUpdatesResponse.updates:type_name -> api.ReleaseInfo @@ -5714,44 +5800,46 @@ var file_sdk_proto_v1_api_proto_depIdxs = []int32{ 56, // 75: api.VailbreakProxy.ValidateOpenstackIp:input_type -> api.ValidateOpenstackIpRequest 58, // 76: api.VailbreakProxy.RevalidateCredentials:input_type -> api.RevalidateCredentialsRequest 60, // 77: api.VailbreakProxy.InjectEnvVariables:input_type -> api.InjectEnvVariablesRequest - 63, // 78: api.VailbreakProxy.CheckNetworkSubnetCompatibility:input_type -> api.CheckNetworkSubnetCompatibilityRequest - 70, // 79: api.StorageArray.ValidateCredentials:input_type -> api.ValidateStorageCredsRequest - 72, // 80: api.StorageArray.CreateOrUpdateInitiatorGroup:input_type -> api.CreateInitiatorGroupRequest - 74, // 81: api.StorageArray.MapVolumeToGroup:input_type -> api.MapVolumeRequest - 76, // 82: api.StorageArray.UnmapVolumeFromGroup:input_type -> api.UnmapVolumeRequest - 78, // 83: api.StorageArray.GetMappedGroups:input_type -> api.GetMappedGroupsRequest - 80, // 84: api.StorageArray.ResolveCinderVolume:input_type -> api.ResolveCinderVolumeRequest - 4, // 85: api.Version.Version:output_type -> api.VersionResponse - 9, // 86: api.Version.InitiateUpgrade:output_type -> api.UpgradeResponse - 10, // 87: api.Version.GetUpgradeProgress:output_type -> api.UpgradeProgressResponse - 6, // 88: api.Version.GetAvailableTags:output_type -> api.AvailableUpdatesResponse - 66, // 89: api.Version.Cleanup:output_type -> api.CleanupResponse - 20, // 90: api.VCenter.ListVMs:output_type -> api.ListVMsResponse - 22, // 91: api.VCenter.GetVM:output_type -> api.GetVMResponse - 24, // 92: api.VCenter.ReclaimVM:output_type -> api.ReclaimVMResponse - 26, // 93: api.VCenter.CordonHost:output_type -> api.CordonHostResponse - 18, // 94: api.VCenter.UnCordonHost:output_type -> api.UnCordonHostResponse - 15, // 95: api.VCenter.ListHosts:output_type -> api.ListHostsResponse - 30, // 96: api.BMProvider.ListMachines:output_type -> api.BMListMachinesResponse - 32, // 97: api.BMProvider.GetResourceInfo:output_type -> api.GetResourceInfoResponse - 34, // 98: api.BMProvider.SetResourcePower:output_type -> api.SetResourcePowerResponse - 36, // 99: api.BMProvider.SetResourceBM2PXEBoot:output_type -> api.SetResourceBM2PXEBootResponse - 38, // 100: api.BMProvider.WhoAmI:output_type -> api.WhoAmIResponse - 41, // 101: api.BMProvider.ListBootSource:output_type -> api.ListBootSourceResponse - 44, // 102: api.BMProvider.ReclaimBMHost:output_type -> api.ReclaimBMResponse - 46, // 103: api.BMProvider.DeployMachine:output_type -> api.DeployMachineResponse - 57, // 104: api.VailbreakProxy.ValidateOpenstackIp:output_type -> api.ValidateOpenstackIpResponse - 59, // 105: api.VailbreakProxy.RevalidateCredentials:output_type -> api.RevalidateCredentialsResponse - 61, // 106: api.VailbreakProxy.InjectEnvVariables:output_type -> api.InjectEnvVariablesResponse - 64, // 107: api.VailbreakProxy.CheckNetworkSubnetCompatibility:output_type -> api.CheckNetworkSubnetCompatibilityResponse - 71, // 108: api.StorageArray.ValidateCredentials:output_type -> api.ValidateStorageCredsResponse - 73, // 109: api.StorageArray.CreateOrUpdateInitiatorGroup:output_type -> api.CreateInitiatorGroupResponse - 75, // 110: api.StorageArray.MapVolumeToGroup:output_type -> api.MapVolumeResponse - 77, // 111: api.StorageArray.UnmapVolumeFromGroup:output_type -> api.UnmapVolumeResponse - 79, // 112: api.StorageArray.GetMappedGroups:output_type -> api.GetMappedGroupsResponse - 81, // 113: api.StorageArray.ResolveCinderVolume:output_type -> api.ResolveCinderVolumeResponse - 85, // [85:114] is the sub-list for method output_type - 56, // [56:85] is the sub-list for method input_type + 82, // 78: api.VailbreakProxy.ApplyTimeSettings:input_type -> api.ApplyTimeSettingsRequest + 63, // 79: api.VailbreakProxy.CheckNetworkSubnetCompatibility:input_type -> api.CheckNetworkSubnetCompatibilityRequest + 70, // 80: api.StorageArray.ValidateCredentials:input_type -> api.ValidateStorageCredsRequest + 72, // 81: api.StorageArray.CreateOrUpdateInitiatorGroup:input_type -> api.CreateInitiatorGroupRequest + 74, // 82: api.StorageArray.MapVolumeToGroup:input_type -> api.MapVolumeRequest + 76, // 83: api.StorageArray.UnmapVolumeFromGroup:input_type -> api.UnmapVolumeRequest + 78, // 84: api.StorageArray.GetMappedGroups:input_type -> api.GetMappedGroupsRequest + 80, // 85: api.StorageArray.ResolveCinderVolume:input_type -> api.ResolveCinderVolumeRequest + 4, // 86: api.Version.Version:output_type -> api.VersionResponse + 9, // 87: api.Version.InitiateUpgrade:output_type -> api.UpgradeResponse + 10, // 88: api.Version.GetUpgradeProgress:output_type -> api.UpgradeProgressResponse + 6, // 89: api.Version.GetAvailableTags:output_type -> api.AvailableUpdatesResponse + 66, // 90: api.Version.Cleanup:output_type -> api.CleanupResponse + 20, // 91: api.VCenter.ListVMs:output_type -> api.ListVMsResponse + 22, // 92: api.VCenter.GetVM:output_type -> api.GetVMResponse + 24, // 93: api.VCenter.ReclaimVM:output_type -> api.ReclaimVMResponse + 26, // 94: api.VCenter.CordonHost:output_type -> api.CordonHostResponse + 18, // 95: api.VCenter.UnCordonHost:output_type -> api.UnCordonHostResponse + 15, // 96: api.VCenter.ListHosts:output_type -> api.ListHostsResponse + 30, // 97: api.BMProvider.ListMachines:output_type -> api.BMListMachinesResponse + 32, // 98: api.BMProvider.GetResourceInfo:output_type -> api.GetResourceInfoResponse + 34, // 99: api.BMProvider.SetResourcePower:output_type -> api.SetResourcePowerResponse + 36, // 100: api.BMProvider.SetResourceBM2PXEBoot:output_type -> api.SetResourceBM2PXEBootResponse + 38, // 101: api.BMProvider.WhoAmI:output_type -> api.WhoAmIResponse + 41, // 102: api.BMProvider.ListBootSource:output_type -> api.ListBootSourceResponse + 44, // 103: api.BMProvider.ReclaimBMHost:output_type -> api.ReclaimBMResponse + 46, // 104: api.BMProvider.DeployMachine:output_type -> api.DeployMachineResponse + 57, // 105: api.VailbreakProxy.ValidateOpenstackIp:output_type -> api.ValidateOpenstackIpResponse + 59, // 106: api.VailbreakProxy.RevalidateCredentials:output_type -> api.RevalidateCredentialsResponse + 61, // 107: api.VailbreakProxy.InjectEnvVariables:output_type -> api.InjectEnvVariablesResponse + 83, // 108: api.VailbreakProxy.ApplyTimeSettings:output_type -> api.ApplyTimeSettingsResponse + 64, // 109: api.VailbreakProxy.CheckNetworkSubnetCompatibility:output_type -> api.CheckNetworkSubnetCompatibilityResponse + 71, // 110: api.StorageArray.ValidateCredentials:output_type -> api.ValidateStorageCredsResponse + 73, // 111: api.StorageArray.CreateOrUpdateInitiatorGroup:output_type -> api.CreateInitiatorGroupResponse + 75, // 112: api.StorageArray.MapVolumeToGroup:output_type -> api.MapVolumeResponse + 77, // 113: api.StorageArray.UnmapVolumeFromGroup:output_type -> api.UnmapVolumeResponse + 79, // 114: api.StorageArray.GetMappedGroups:output_type -> api.GetMappedGroupsResponse + 81, // 115: api.StorageArray.ResolveCinderVolume:output_type -> api.ResolveCinderVolumeResponse + 86, // [86:116] is the sub-list for method output_type + 56, // [56:86] is the sub-list for method input_type 56, // [56:56] is the sub-list for extension type_name 56, // [56:56] is the sub-list for extension extendee 0, // [0:56] is the sub-list for field type_name @@ -5789,7 +5877,7 @@ func file_sdk_proto_v1_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sdk_proto_v1_api_proto_rawDesc), len(file_sdk_proto_v1_api_proto_rawDesc)), NumEnums: 2, - NumMessages: 80, + NumMessages: 82, NumExtensions: 0, NumServices: 5, }, diff --git a/pkg/vpwned/api/proto/v1/service/api.pb.gw.go b/pkg/vpwned/api/proto/v1/service/api.pb.gw.go index 1addd5172..b63b1b901 100644 --- a/pkg/vpwned/api/proto/v1/service/api.pb.gw.go +++ b/pkg/vpwned/api/proto/v1/service/api.pb.gw.go @@ -653,6 +653,33 @@ func local_request_VailbreakProxy_InjectEnvVariables_0(ctx context.Context, mars return msg, metadata, err } +func request_VailbreakProxy_ApplyTimeSettings_0(ctx context.Context, marshaler runtime.Marshaler, client VailbreakProxyClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ApplyTimeSettingsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ApplyTimeSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_VailbreakProxy_ApplyTimeSettings_0(ctx context.Context, marshaler runtime.Marshaler, server VailbreakProxyServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ApplyTimeSettingsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ApplyTimeSettings(ctx, &protoReq) + return msg, metadata, err +} + func request_VailbreakProxy_CheckNetworkSubnetCompatibility_0(ctx context.Context, marshaler runtime.Marshaler, client VailbreakProxyClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CheckNetworkSubnetCompatibilityRequest @@ -1318,6 +1345,26 @@ func RegisterVailbreakProxyHandlerServer(ctx context.Context, mux *runtime.Serve } forward_VailbreakProxy_InjectEnvVariables_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_VailbreakProxy_ApplyTimeSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/api.VailbreakProxy/ApplyTimeSettings", runtime.WithHTTPPathPattern("/vpw/v1/settings/apply-time-settings")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_VailbreakProxy_ApplyTimeSettings_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_VailbreakProxy_ApplyTimeSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_VailbreakProxy_CheckNetworkSubnetCompatibility_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2055,6 +2102,23 @@ func RegisterVailbreakProxyHandlerClient(ctx context.Context, mux *runtime.Serve } forward_VailbreakProxy_InjectEnvVariables_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_VailbreakProxy_ApplyTimeSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/api.VailbreakProxy/ApplyTimeSettings", runtime.WithHTTPPathPattern("/vpw/v1/settings/apply-time-settings")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_VailbreakProxy_ApplyTimeSettings_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_VailbreakProxy_ApplyTimeSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_VailbreakProxy_CheckNetworkSubnetCompatibility_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2079,6 +2143,7 @@ var ( pattern_VailbreakProxy_ValidateOpenstackIp_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"vpw", "v1", "validate_openstack_ip"}, "")) pattern_VailbreakProxy_RevalidateCredentials_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"vpw", "v1", "revalidate_credentials"}, "")) pattern_VailbreakProxy_InjectEnvVariables_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"vpw", "v1", "inject_env_variables"}, "")) + pattern_VailbreakProxy_ApplyTimeSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"vpw", "v1", "settings", "apply-time-settings"}, "")) pattern_VailbreakProxy_CheckNetworkSubnetCompatibility_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"vpw", "v1", "check_network_subnet_compatibility"}, "")) ) @@ -2086,6 +2151,7 @@ var ( forward_VailbreakProxy_ValidateOpenstackIp_0 = runtime.ForwardResponseMessage forward_VailbreakProxy_RevalidateCredentials_0 = runtime.ForwardResponseMessage forward_VailbreakProxy_InjectEnvVariables_0 = runtime.ForwardResponseMessage + forward_VailbreakProxy_ApplyTimeSettings_0 = runtime.ForwardResponseMessage forward_VailbreakProxy_CheckNetworkSubnetCompatibility_0 = runtime.ForwardResponseMessage ) diff --git a/pkg/vpwned/api/proto/v1/service/api_grpc.pb.go b/pkg/vpwned/api/proto/v1/service/api_grpc.pb.go index ade432e3b..17e730cea 100644 --- a/pkg/vpwned/api/proto/v1/service/api_grpc.pb.go +++ b/pkg/vpwned/api/proto/v1/service/api_grpc.pb.go @@ -939,6 +939,7 @@ const ( VailbreakProxy_ValidateOpenstackIp_FullMethodName = "/api.VailbreakProxy/ValidateOpenstackIp" VailbreakProxy_RevalidateCredentials_FullMethodName = "/api.VailbreakProxy/RevalidateCredentials" VailbreakProxy_InjectEnvVariables_FullMethodName = "/api.VailbreakProxy/InjectEnvVariables" + VailbreakProxy_ApplyTimeSettings_FullMethodName = "/api.VailbreakProxy/ApplyTimeSettings" VailbreakProxy_CheckNetworkSubnetCompatibility_FullMethodName = "/api.VailbreakProxy/CheckNetworkSubnetCompatibility" ) @@ -949,6 +950,7 @@ type VailbreakProxyClient interface { ValidateOpenstackIp(ctx context.Context, in *ValidateOpenstackIpRequest, opts ...grpc.CallOption) (*ValidateOpenstackIpResponse, error) RevalidateCredentials(ctx context.Context, in *RevalidateCredentialsRequest, opts ...grpc.CallOption) (*RevalidateCredentialsResponse, error) InjectEnvVariables(ctx context.Context, in *InjectEnvVariablesRequest, opts ...grpc.CallOption) (*InjectEnvVariablesResponse, error) + ApplyTimeSettings(ctx context.Context, in *ApplyTimeSettingsRequest, opts ...grpc.CallOption) (*ApplyTimeSettingsResponse, error) CheckNetworkSubnetCompatibility(ctx context.Context, in *CheckNetworkSubnetCompatibilityRequest, opts ...grpc.CallOption) (*CheckNetworkSubnetCompatibilityResponse, error) } @@ -990,6 +992,16 @@ func (c *vailbreakProxyClient) InjectEnvVariables(ctx context.Context, in *Injec return out, nil } +func (c *vailbreakProxyClient) ApplyTimeSettings(ctx context.Context, in *ApplyTimeSettingsRequest, opts ...grpc.CallOption) (*ApplyTimeSettingsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyTimeSettingsResponse) + err := c.cc.Invoke(ctx, VailbreakProxy_ApplyTimeSettings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *vailbreakProxyClient) CheckNetworkSubnetCompatibility(ctx context.Context, in *CheckNetworkSubnetCompatibilityRequest, opts ...grpc.CallOption) (*CheckNetworkSubnetCompatibilityResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CheckNetworkSubnetCompatibilityResponse) @@ -1007,6 +1019,7 @@ type VailbreakProxyServer interface { ValidateOpenstackIp(context.Context, *ValidateOpenstackIpRequest) (*ValidateOpenstackIpResponse, error) RevalidateCredentials(context.Context, *RevalidateCredentialsRequest) (*RevalidateCredentialsResponse, error) InjectEnvVariables(context.Context, *InjectEnvVariablesRequest) (*InjectEnvVariablesResponse, error) + ApplyTimeSettings(context.Context, *ApplyTimeSettingsRequest) (*ApplyTimeSettingsResponse, error) CheckNetworkSubnetCompatibility(context.Context, *CheckNetworkSubnetCompatibilityRequest) (*CheckNetworkSubnetCompatibilityResponse, error) mustEmbedUnimplementedVailbreakProxyServer() } @@ -1027,6 +1040,9 @@ func (UnimplementedVailbreakProxyServer) RevalidateCredentials(context.Context, func (UnimplementedVailbreakProxyServer) InjectEnvVariables(context.Context, *InjectEnvVariablesRequest) (*InjectEnvVariablesResponse, error) { return nil, status.Error(codes.Unimplemented, "method InjectEnvVariables not implemented") } +func (UnimplementedVailbreakProxyServer) ApplyTimeSettings(context.Context, *ApplyTimeSettingsRequest) (*ApplyTimeSettingsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyTimeSettings not implemented") +} func (UnimplementedVailbreakProxyServer) CheckNetworkSubnetCompatibility(context.Context, *CheckNetworkSubnetCompatibilityRequest) (*CheckNetworkSubnetCompatibilityResponse, error) { return nil, status.Error(codes.Unimplemented, "method CheckNetworkSubnetCompatibility not implemented") } @@ -1105,6 +1121,24 @@ func _VailbreakProxy_InjectEnvVariables_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _VailbreakProxy_ApplyTimeSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyTimeSettingsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VailbreakProxyServer).ApplyTimeSettings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VailbreakProxy_ApplyTimeSettings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VailbreakProxyServer).ApplyTimeSettings(ctx, req.(*ApplyTimeSettingsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _VailbreakProxy_CheckNetworkSubnetCompatibility_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CheckNetworkSubnetCompatibilityRequest) if err := dec(in); err != nil { @@ -1142,6 +1176,10 @@ var VailbreakProxy_ServiceDesc = grpc.ServiceDesc{ MethodName: "InjectEnvVariables", Handler: _VailbreakProxy_InjectEnvVariables_Handler, }, + { + MethodName: "ApplyTimeSettings", + Handler: _VailbreakProxy_ApplyTimeSettings_Handler, + }, { MethodName: "CheckNetworkSubnetCompatibility", Handler: _VailbreakProxy_CheckNetworkSubnetCompatibility_Handler, diff --git a/pkg/vpwned/deploy/k8s.svc.yaml b/pkg/vpwned/deploy/k8s.svc.yaml index 621a487d5..8bf308810 100644 --- a/pkg/vpwned/deploy/k8s.svc.yaml +++ b/pkg/vpwned/deploy/k8s.svc.yaml @@ -46,6 +46,7 @@ spec: envFrom: - configMapRef: name: pf9-env + optional: true dnsConfig: options: - name: ndots diff --git a/pkg/vpwned/openapiv3/dist/apidocs.swagger.json b/pkg/vpwned/openapiv3/dist/apidocs.swagger.json index 8978355d7..43c158ddb 100644 --- a/pkg/vpwned/openapiv3/dist/apidocs.swagger.json +++ b/pkg/vpwned/openapiv3/dist/apidocs.swagger.json @@ -793,6 +793,38 @@ ] } }, + "/vpw/v1/settings/apply-time-settings": { + "post": { + "operationId": "VailbreakProxy_ApplyTimeSettings", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/apiApplyTimeSettingsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/apiApplyTimeSettingsRequest" + } + } + ], + "tags": [ + "VailbreakProxy" + ] + } + }, "/vpw/v1/storage/initiator_group": { "post": { "operationId": "StorageArray_CreateOrUpdateInitiatorGroup", @@ -1171,6 +1203,17 @@ } }, "definitions": { + "apiApplyTimeSettingsRequest": { + "type": "object" + }, + "apiApplyTimeSettingsResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, "apiAvailableUpdatesResponse": { "type": "object", "properties": { diff --git a/pkg/vpwned/sdk/proto/v1/api.proto b/pkg/vpwned/sdk/proto/v1/api.proto index 5b00a5f52..5152817a1 100644 --- a/pkg/vpwned/sdk/proto/v1/api.proto +++ b/pkg/vpwned/sdk/proto/v1/api.proto @@ -558,6 +558,13 @@ service VailbreakProxy { }; } + rpc ApplyTimeSettings(ApplyTimeSettingsRequest) returns (ApplyTimeSettingsResponse) { + option (google.api.http) = { + post: "/vpw/v1/settings/apply-time-settings" + body: "*" + }; + } + rpc CheckNetworkSubnetCompatibility(CheckNetworkSubnetCompatibilityRequest) returns (CheckNetworkSubnetCompatibilityResponse) { option (google.api.http) = { post: "/vpw/v1/check_network_subnet_compatibility" @@ -706,3 +713,10 @@ message ResolveCinderVolumeResponse { string message = 2; VolumeInfo volume = 3; } + +message ApplyTimeSettingsRequest { +} + +message ApplyTimeSettingsResponse { + string message = 1; +} diff --git a/pkg/vpwned/server/version.go b/pkg/vpwned/server/version.go index 912cee4a4..8086081e4 100644 --- a/pkg/vpwned/server/version.go +++ b/pkg/vpwned/server/version.go @@ -330,6 +330,7 @@ func createUpgradeJob(ctx context.Context, kubeClient client.Client, targetVersi backoffLimit := int32(0) ttlSeconds := int32(86400) activeDeadlineSeconds := int64(3600) + optionalConfigMap := true autoCleanupStr := "false" if autoCleanup { @@ -366,6 +367,16 @@ func createUpgradeJob(ctx context.Context, kubeClient client.Client, targetVersi Image: vpwnedImage, ImagePullPolicy: corev1.PullAlways, Command: []string{"./vpwctl", "upgrade-job"}, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "pf9-env", + }, + Optional: &optionalConfigMap, + }, + }, + }, Env: []corev1.EnvVar{ { Name: "UPGRADE_TARGET_VERSION", @@ -440,6 +451,7 @@ func createRollbackJob(ctx context.Context, kubeClient client.Client, previousVe backoffLimit := int32(0) ttlSeconds := int32(86400) activeDeadlineSeconds := int64(3600) + optionalConfigMap := true job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -471,6 +483,16 @@ func createRollbackJob(ctx context.Context, kubeClient client.Client, previousVe Image: vpwnedImage, ImagePullPolicy: corev1.PullAlways, Command: []string{"./vpwctl", "upgrade-job"}, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "pf9-env", + }, + Optional: &optionalConfigMap, + }, + }, + }, Env: []corev1.EnvVar{ { Name: "UPGRADE_TARGET_VERSION", diff --git a/pkg/vpwned/server/vjailbreak_proxy.go b/pkg/vpwned/server/vjailbreak_proxy.go index ddfbdcdb0..20595936a 100644 --- a/pkg/vpwned/server/vjailbreak_proxy.go +++ b/pkg/vpwned/server/vjailbreak_proxy.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "os/exec" "net" "net/http" "strings" @@ -35,6 +36,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" ) +const applyTimeSettingsScript = "/etc/pf9/apply-time-settings.sh" + type vjailbreakProxy struct { api.UnimplementedVailbreakProxyServer K8sClient client.Client @@ -773,6 +776,25 @@ func (p *vjailbreakProxy) InjectEnvVariables(ctx context.Context, in *api.Inject }, nil } +// ApplyTimeSettings runs the apply-time-settings.sh script synchronously via nsenter and +// returns an error if the script fails so the caller gets accurate status. +func (p *vjailbreakProxy) ApplyTimeSettings(_ context.Context, _ *api.ApplyTimeSettingsRequest) (*api.ApplyTimeSettingsResponse, error) { + const fn = "ApplyTimeSettings" + logrus.WithField("func", fn).Info("Running apply-time-settings.sh on host via nsenter") + + cmd := exec.Command("nsenter", "--target", "1", "--mount", "--", applyTimeSettingsScript) + out, err := cmd.CombinedOutput() + if err != nil { + logrus.WithField("func", fn).WithError(err).Errorf("apply-time-settings.sh failed: %s", string(out)) + return nil, fmt.Errorf("apply-time-settings.sh failed: %w: %s", err, string(out)) + } + + logrus.WithField("func", fn).Infof("apply-time-settings.sh completed: %s", string(out)) + return &api.ApplyTimeSettingsResponse{ + Message: "Time settings applied successfully", + }, nil +} + // checkNetworkSubnetCompatibilityRequest is the request body for CheckNetworkSubnetCompatibility type checkNetworkSubnetCompatibilityRequest struct { Ips []string `json:"ips"` diff --git a/ui/deploy/ui.yaml b/ui/deploy/ui.yaml index b35446c47..50749c0fe 100644 --- a/ui/deploy/ui.yaml +++ b/ui/deploy/ui.yaml @@ -27,6 +27,10 @@ spec: imagePullPolicy: IfNotPresent ports: - containerPort: 80 + envFrom: + - configMapRef: + name: pf9-env + optional: true volumeMounts: - name: nginx-shadow-htpasswd mountPath: /etc/nginx/shadow diff --git a/ui/src/api/settings/model.ts b/ui/src/api/settings/model.ts index 1c4ec82af..2705a31ae 100644 --- a/ui/src/api/settings/model.ts +++ b/ui/src/api/settings/model.ts @@ -8,6 +8,8 @@ export interface VjailbreakSettings { CLEANUP_PORTS_AFTER_MIGRATION_FAILURE: string DEFAULT_MIGRATION_METHOD: string DEPLOYMENT_NAME: string + TIMEZONE?: string + NTP_SERVERS?: string PROXY?: string POPULATE_VMWARE_MACHINE_FLAVORS: string VCENTER_LOGIN_RETRY_LIMIT: number diff --git a/ui/src/api/settings/settings.ts b/ui/src/api/settings/settings.ts index d60287bfb..85795a614 100644 --- a/ui/src/api/settings/settings.ts +++ b/ui/src/api/settings/settings.ts @@ -1,4 +1,4 @@ -import { get, put } from '../axios' +import { get, post, put } from '../axios' import { VjailbreakSettings } from './model' export const VERSION_CONFIG_MAP_NAME = 'vjailbreak-settings' @@ -36,3 +36,11 @@ export const updateSettingsConfigMap = async ( }) return response } + +export const applyTimeSettings = async (): Promise => { + await post({ + endpoint: '/dev-api/sdk/vpw/v1/settings/apply-time-settings', + data: {}, + config: { mock: false } + }) +} diff --git a/ui/src/features/globalSettings/components/GlobalSettingsPage.tsx b/ui/src/features/globalSettings/components/GlobalSettingsPage.tsx index a0044294f..fb6eebe23 100644 --- a/ui/src/features/globalSettings/components/GlobalSettingsPage.tsx +++ b/ui/src/features/globalSettings/components/GlobalSettingsPage.tsx @@ -1,6 +1,7 @@ import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useLocation } from 'react-router-dom' +import { POPULAR_TIMEZONES, type TimezoneOption } from '../timezones' import { Alert, Box, @@ -30,9 +31,14 @@ import InlineHelp from 'src/components/design-system/ui/InlineHelp' import ToggleField from 'src/components/design-system/ui/ToggleField' import VDDKUploadTab from './VDDKUploadTab' import type { VddkUploadStatus } from './VDDKUploadTab' -import { IntervalField as SharedIntervalField, RHFTextField } from 'src/shared/components/forms' +import { + IntervalField as SharedIntervalField, + RHFAutocomplete, + RHFTextField +} from 'src/shared/components/forms' import { getGlobalSettingsHelpers, type SettingsForm } from 'src/features/globalSettings/helpers' import { + applyTimeSettings, getSettingsConfigMap, updateSettingsConfigMap, VERSION_CONFIG_MAP_NAME, @@ -42,6 +48,8 @@ import { getPf9EnvConfig, injectEnvVariables } from 'src/api/helpers' import { CloudUploadOutlined } from '@mui/icons-material' import { uploadVddkFile } from 'src/api/vddk' import { useVddkStatusQuery } from 'src/hooks/api/useVddkStatusQuery' +import { useMigrationsQuery } from 'src/hooks/api/useMigrationsQuery' +import { Phase } from 'src/api/migrations/model' import axios from 'axios' const VDDK_UPLOADED_KEY = 'vddk-uploaded' @@ -83,6 +91,8 @@ const DEFAULTS: SettingsForm = { VALIDATE_RDM_OWNER_VMS: true, AUTO_FSTAB_UPDATE: true, DEPLOYMENT_NAME: 'vJailbreak', + TIMEZONE: '', + NTP_SERVERS: '', PROXY_ENABLED: false, PROXY_HTTP_SCHEME: 'http', PROXY_HTTP_HOST: '', @@ -97,7 +107,12 @@ const helpers = getGlobalSettingsHelpers(DEFAULTS) type TabKey = 'general' | 'retry' | 'network' | 'advanced' | 'vddk' const TAB_FIELD_KEYS: Record> = { - general: ['DEPLOYMENT_NAME', 'CHANGED_BLOCKS_COPY_ITERATION_THRESHOLD', 'PERIODIC_SYNC_INTERVAL'], + general: [ + 'DEPLOYMENT_NAME', + 'TIMEZONE', + 'CHANGED_BLOCKS_COPY_ITERATION_THRESHOLD', + 'PERIODIC_SYNC_INTERVAL' + ], retry: [ 'VM_ACTIVE_WAIT_INTERVAL_SECONDS', 'VM_ACTIVE_WAIT_RETRY_LIMIT', @@ -120,6 +135,7 @@ const TAB_FIELD_KEYS: Record> = { 'OPENSTACK_CREDS_REQUEUE_AFTER_MINUTES', 'VMWARE_CREDS_REQUEUE_AFTER_MINUTES', 'DEFAULT_MIGRATION_METHOD', + 'NTP_SERVERS', 'CLEANUP_VOLUMES_AFTER_CONVERT_FAILURE', 'CLEANUP_PORTS_AFTER_MIGRATION_FAILURE', 'POPULATE_VMWARE_MACHINE_FLAVORS', @@ -160,6 +176,29 @@ const TAB_META: Record { + const v = value.trim() + if (!v) return false + if (v.includes('://') || v.includes('/')) return false + + const ipv4Match = v.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) + if (ipv4Match) { + const octets = ipv4Match.slice(1).map((part) => Number(part)) + return octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255) + } + + if (!/^[a-zA-Z0-9.-]+$/.test(v)) return false + if (v.startsWith('.') || v.endsWith('.') || v.includes('..')) return false + + return v.split('.').every((label) => { + if (!label) return false + if (label.length > 63) return false + if (!/^[a-zA-Z0-9-]+$/.test(label)) return false + if (label.startsWith('-') || label.endsWith('-')) return false + return true + }) +} + const TabLabel = ({ label, showError, @@ -210,6 +249,10 @@ const TabPanel = ({ const FIELD_TOOLTIPS: Record = { DEPLOYMENT_NAME: 'Display name shown across dashboards and exported workflows.', + TIMEZONE: + 'Select a timezone for the vJailbreak VM. If NTP Servers is empty, leaving this blank disables time synchronization; selecting a timezone enables time synchronization and updates the timezone.', + NTP_SERVERS: + 'Optional comma or newline separated NTP servers. When set, these override default public pools. Time sync remains enabled even if Timezone is blank (defaults to UTC).', CHANGED_BLOCKS_COPY_ITERATION_THRESHOLD: 'Number of iterations to copy changed blocks.', PERIODIC_SYNC_INTERVAL: 'Frequency for background periodic sync jobs (minimum 5 minutes).', VM_ACTIVE_WAIT_INTERVAL_SECONDS: 'Interval to wait for VM to become active (in seconds).', @@ -307,6 +350,17 @@ const EMPTY_ERRORS: FieldErrorMap = {} const { parseInterval, validateProxyUrl, deriveProxyState, applyProxyState } = helpers const { toConfigMapData, fromConfigMapData, buildEnvPayload } = helpers +const isValidTimezone = (value: string): boolean => { + const tz = value.trim() + if (!tz) return false + try { + new Intl.DateTimeFormat('en-US', { timeZone: tz }).format(new Date()) + return true + } catch { + return false + } +} + type UseGlobalSettingsControllerReturn = { form: SettingsForm errors: FieldErrorMap @@ -317,13 +371,14 @@ type UseGlobalSettingsControllerReturn = { setActiveTab: React.Dispatch> notification: NotificationState proxyUpdateSuccess: boolean + timezoneOptions: TimezoneOption[] + isTimeSettingsDisabled: boolean onText: (e: React.ChangeEvent) => void onBool: (e: React.ChangeEvent) => void onSelect: (e: SelectChangeEvent) => void tabHasError: (tab: TabKey) => boolean handleTabChange: (_: SyntheticEvent, value: string | number) => void onResetDefaults: () => void - onCancel: () => void onSave: (e: React.FormEvent) => Promise handleNotificationClose: (_: SyntheticEvent | Event, reason?: SnackbarCloseReason) => void } @@ -343,6 +398,35 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { }) const form = rhfForm.watch() as SettingsForm + const TERMINAL_PHASES = useMemo( + () => new Set([Phase.Succeeded, Phase.Failed, Phase.ValidationFailed, Phase.Unknown]), + [] + ) + const { data: migrations } = useMigrationsQuery(undefined, { refetchInterval: 30000 }) + const isTimeSettingsDisabled = useMemo( + () => (migrations ?? []).some((m) => !TERMINAL_PHASES.has(m.status?.phase as string)), + [migrations, TERMINAL_PHASES] + ) + + const timezoneOptions = useMemo(() => { + const tz = (form.TIMEZONE ?? '').trim() + if (!tz || POPULAR_TIMEZONES.some((opt) => opt.value === tz)) { + return POPULAR_TIMEZONES + } + + if (!isValidTimezone(tz)) { + return POPULAR_TIMEZONES + } + + return [ + { + value: tz, + offset: '', + label: `(Legacy) ${tz}` + }, + ...POPULAR_TIMEZONES + ] + }, [form.TIMEZONE]) const buildErrors = useCallback((state: SettingsForm): FieldErrorMap => { const e: FieldErrorMap = {} @@ -405,6 +489,26 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { e.DEPLOYMENT_NAME = 'Must be 63 characters or fewer.' } + const tz = (state.TIMEZONE ?? '').trim() + if (tz) { + const tzOk = POPULAR_TIMEZONES.some((opt) => opt.value === tz) || isValidTimezone(tz) + if (!tzOk) e.TIMEZONE = 'Select a timezone from the list.' + } + + const ntpRaw = state.NTP_SERVERS ?? '' + if (ntpRaw.trim()) { + const ntpEntries = ntpRaw + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter(Boolean) + + const invalidNtpEntry = ntpEntries.find((entry) => !isValidNtpServer(entry)) + if (invalidNtpEntry) { + e.NTP_SERVERS = + `Invalid NTP server "${invalidNtpEntry}". Use hostnames or IPv4 addresses, separated by commas or new lines.` + } + } + const proxyEnabled = state.PROXY_ENABLED const httpScheme = state.PROXY_HTTP_SCHEME const httpHost = (state.PROXY_HTTP_HOST ?? '').trim() @@ -440,7 +544,7 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { if (!hostname) return false // permissive hostname / domain check (supports internal hostnames and FQDNs) - if (!/^[a-zA-Z0-9-\.]+$/.test(hostname)) return false + if (!/^[a-zA-Z0-9.-]+$/.test(hostname)) return false if (hostname.startsWith('-') || hostname.endsWith('-')) return false if (hostname.includes('..')) return false @@ -558,8 +662,13 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { const base = fromConfigMapData( settingsCm?.data as Record | undefined ) + const tz = (base.TIMEZONE ?? '').trim() + const normalizedBase = + !tz || POPULAR_TIMEZONES.some((opt) => opt.value === tz) || isValidTimezone(tz) + ? base + : { ...base, TIMEZONE: '' } const proxyState = deriveProxyState(base, pf9Env?.data) - const merged = applyProxyState(base, proxyState) + const merged = applyProxyState(normalizedBase, proxyState) rhfForm.reset(merged) setInitial(merged) @@ -619,10 +728,6 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { rhfForm.reset({ ...DEFAULTS }) }, [rhfForm]) - const onCancel = useCallback(() => { - rhfForm.reset({ ...initial }) - }, [initial, rhfForm]) - const onSave = useCallback( async (e: React.FormEvent) => { e.preventDefault() @@ -643,11 +748,24 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { form.PROXY_HTTPS_PORT !== initial.PROXY_HTTPS_PORT || form.NO_PROXY !== initial.NO_PROXY + const timeSettingsChanged = + form.TIMEZONE !== initial.TIMEZONE || form.NTP_SERVERS !== initial.NTP_SERVERS + + if (timeSettingsChanged) { + show('Applying time settings...', 'info') + } + setSaving(true) try { const existingCm = await getSettingsConfigMap() const updatedData = toConfigMapData(form) + const mergedData: Record = { + ...(existingCm?.data as any), + ...updatedData + } + + delete mergedData.NTP_ENABLED await updateSettingsConfigMap({ apiVersion: existingCm?.apiVersion || 'v1', kind: existingCm?.kind || 'ConfigMap', @@ -657,11 +775,14 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { namespace: VERSION_NAMESPACE }, data: { - ...(existingCm?.data as any), - ...updatedData + ...mergedData } } as any) + if (timeSettingsChanged) { + await applyTimeSettings() + } + let envInjectionFailed = false try { @@ -696,11 +817,21 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { if (proxyChanged) { setProxyUpdateSuccess(true) } - show('Global Settings saved successfully.', 'success') + show( + timeSettingsChanged + ? 'Time settings applied successfully.' + : 'Global Settings saved successfully.', + 'success' + ) } } catch (err) { - console.error('Failed to save Global Settings ConfigMap:', err) - show('Failed to save Global Settings. No changes were applied.', 'error') + console.error('Failed to save Global Settings:', err) + const msg = err instanceof Error ? err.message : String(err) + if (timeSettingsChanged && msg) { + show(`Failed to apply time settings: ${msg}`, 'error') + } else { + show('Failed to save Global Settings. No changes were applied.', 'error') + } } finally { setSaving(false) } @@ -736,13 +867,14 @@ const useGlobalSettingsController = (): UseGlobalSettingsControllerReturn => { setActiveTab, notification, proxyUpdateSuccess, + timezoneOptions, + isTimeSettingsDisabled, onText, onBool, onSelect, tabHasError, handleTabChange, onResetDefaults, - onCancel, onSave, handleNotificationClose } @@ -768,7 +900,9 @@ export default function GlobalSettingsPage() { handleTabChange, onResetDefaults, onSave, - handleNotificationClose + handleNotificationClose, + timezoneOptions, + isTimeSettingsDisabled, } = useGlobalSettingsController() const activeTabRef = useRef(activeTab) @@ -1057,6 +1191,27 @@ export default function GlobalSettingsPage() { helperText={errors.DEPLOYMENT_NAME} /> + + name="TIMEZONE" + options={timezoneOptions} + label="Timezone" + placeholder="Search or select a timezone" + disableClearable={false} + clearOnEscape={true} + getOptionLabel={(option) => option.label} + getOptionValue={(option) => option.value} + renderOptionLabel={(option) => option.label} + error={Boolean(errors.TIMEZONE)} + helperText={errors.TIMEZONE} + disabled={isTimeSettingsDisabled} + data-testid="global-settings-field-TIMEZONE" + labelProps={{ + tooltip: isTimeSettingsDisabled + ? 'Cannot change timezone while migrations are in progress.' + : FIELD_TOOLTIPS.TIMEZONE + }} + /> + + + { VMWARE_CREDS_REQUEUE_AFTER_MINUTES: String(f.VMWARE_CREDS_REQUEUE_AFTER_MINUTES), VALIDATE_RDM_OWNER_VMS: String(f.VALIDATE_RDM_OWNER_VMS), AUTO_FSTAB_UPDATE: String(f.AUTO_FSTAB_UPDATE), - DEPLOYMENT_NAME: f.DEPLOYMENT_NAME + DEPLOYMENT_NAME: f.DEPLOYMENT_NAME, + TIMEZONE: f.TIMEZONE, + NTP_SERVERS: f.NTP_SERVERS }) const fromConfigMapData = ( @@ -244,6 +248,9 @@ export const getGlobalSettingsHelpers = (defaults: SettingsForm) => { AUTO_FSTAB_UPDATE: parseBool(data?.AUTO_FSTAB_UPDATE, defaults.AUTO_FSTAB_UPDATE), DEPLOYMENT_NAME: typeof data?.DEPLOYMENT_NAME === 'string' ? data.DEPLOYMENT_NAME : defaults.DEPLOYMENT_NAME, + TIMEZONE: typeof data?.TIMEZONE === 'string' ? data.TIMEZONE : defaults.TIMEZONE, + NTP_SERVERS: + typeof data?.NTP_SERVERS === 'string' ? data.NTP_SERVERS : defaults.NTP_SERVERS, PROXY_ENABLED: defaults.PROXY_ENABLED, PROXY_HTTP_SCHEME: defaults.PROXY_HTTP_SCHEME, PROXY_HTTP_HOST: defaults.PROXY_HTTP_HOST, diff --git a/ui/src/features/globalSettings/timezones.ts b/ui/src/features/globalSettings/timezones.ts new file mode 100644 index 000000000..87678965d --- /dev/null +++ b/ui/src/features/globalSettings/timezones.ts @@ -0,0 +1,57 @@ +export interface TimezoneOption { + label: string + value: string + offset: string +} + +type SupportedValuesOf = (key: 'timeZone') => string[] + +const getOffsetValue = (timeZone: string): { label: string; offset: string } => { + try { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone, + timeZoneName: 'shortOffset', + hour: '2-digit', + minute: '2-digit', + hour12: false + }).formatToParts(new Date()) + + const tzName = parts.find((part) => part.type === 'timeZoneName')?.value ?? 'GMT' + + if (tzName === 'GMT' || tzName === 'UTC') { + return { label: 'UTC+00:00', offset: '+00:00' } + } + + const match = tzName.match(/^GMT(?[+-])(?\d{1,2})(?::?(?\d{2}))?$/) + if (!match?.groups?.sign || !match.groups.hours) { + return { label: 'UTC+00:00', offset: '+00:00' } + } + + const sign = match.groups.sign + const hours = match.groups.hours.padStart(2, '0') + const minutes = (match.groups.minutes ?? '00').padStart(2, '0') + return { label: `UTC${sign}${hours}:${minutes}`, offset: `${sign}${hours}:${minutes}` } + } catch { + return { label: 'UTC+00:00', offset: '+00:00' } + } +} + +const buildTimezoneOptions = (): TimezoneOption[] => { + const supportedValuesOf = (Intl as typeof Intl & { supportedValuesOf?: SupportedValuesOf }) + .supportedValuesOf + + const values = supportedValuesOf ? supportedValuesOf('timeZone') : [] + + return [...new Set([...values, 'UTC'])] + .sort((a, b) => a.localeCompare(b)) + .map((value) => { + const { label, offset } = getOffsetValue(value) + return { + value, + offset, + label: `(${label}) ${value}` + } + }) +} + +export const POPULAR_TIMEZONES: TimezoneOption[] = buildTimezoneOptions() diff --git a/ui/src/shared/components/forms/rhf/RHFAutocomplete.tsx b/ui/src/shared/components/forms/rhf/RHFAutocomplete.tsx index 6d8d72481..87ef58f4e 100644 --- a/ui/src/shared/components/forms/rhf/RHFAutocomplete.tsx +++ b/ui/src/shared/components/forms/rhf/RHFAutocomplete.tsx @@ -12,6 +12,8 @@ export type RHFAutocompleteProps = { name: ControllerProps['name'] options: TOption[] multiple?: boolean + disableClearable?: boolean + clearOnEscape?: boolean label?: string labelHelperText?: FieldLabelProps['helperText'] labelProps?: FieldLabelCustomProps @@ -32,6 +34,8 @@ export default function RHFAutocomplete({ name, options, multiple = false, + disableClearable = false, + clearOnEscape = false, label, labelHelperText, labelProps, @@ -87,6 +91,8 @@ export default function RHFAutocomplete({ multiple={multiple} options={options} disabled={disabled} + disableClearable={disableClearable} + clearOnEscape={clearOnEscape} value={selectedOptions as any} onChange={(_e, value) => { if (multiple) { diff --git a/v2v-helper/pkg/k8sutils/types.go b/v2v-helper/pkg/k8sutils/types.go index e3d4d7482..1abe551f1 100644 --- a/v2v-helper/pkg/k8sutils/types.go +++ b/v2v-helper/pkg/k8sutils/types.go @@ -27,4 +27,6 @@ type VjailbreakSettings struct { V2VHelperPodMemoryLimit string V2VHelperPodEphemeralStorageRequest string V2VHelperPodEphemeralStorageLimit string + Timezone string + NTPServers string }