Cherry pick of #23094 upstream release 4.0 1770106850 (#24452)

* feat(region, climc): Add kickstart parameters and metadata

- Add kickstart parameters and metadata support
- Add kickstart CLI options and management commands
- Set kickstart metadata in PostCreate
- Add related kickstart metadata fields

* feat(host): Implement automated VM installation

- Mounts the installation ISO to a unique temp path, reusing existing mounts.
- Extracts kernel (`vmlinuz`, `initrd`) to configure QEMU boot.
- Injects kernel parameters to trigger the autoinstall.
- Add kickstart configuration validation.

Modified `guestLauncher` script to handle kernel parameters with spaces
that were previously truncated by the Python script.

* feat(host): Implement Kickstart monitoring

- Automatically updates the VM status upon
receiving `KICKSTART_SUCCESS` or `KICKSTART_FAILED` signals.
- On success, cleans up temporary mount points
and restarts the server via the Region API.

* feat(region): add kickstart type metadata

Adds the `VM_METADATA_KICKSTART_TYPE` to distinguish between
kickstart configurations passed by URL or by direct content.

* feat(host): Handle kickstart installing and timeout

- Set status to 'installing' upon successful kickstart boot
- Handle KICKSTART_INSTALLING serial message for status updates
- Add timeout handling for kickstart status monitoring

* feat(host): provide Kickstart config via content

Embed the provided config content as a file in a
labeled ISO (mkisofs), then mount the ISO during
the Kickstart installation to supply the config.

* refactor(host): Enhance the kickstart installation

- Integrates kickstart status directly into the VM status, replacing metadata.
- Unifies locations for kickstart logs and temporary files.
- Uses regex for more robust status matching.
- Moves helper functions to `kickstart_helper.go`.
- Fixes an incorrect kickstart configuration filename.
- Adjusts kickstart logging.

* feat(region, host): add kickstart support for openEuler

* refactor(host): extract kickstart cleanup function

* feat(region, climc): add kickstart-complete cli

Add this command to manually update the kickstart status and
reboot VM if needed. This is mainly used by users to manually
update the kickstart status when kickstart status detection is abnormal.

* refactor(region): reduce code duplication in Kickstart validation

- unify two validateKickstartConfig functions
- extract determineKickstartType function for type determination

* fix(region, host): fix kickstart auto-reboot

- Adjusted the timing of setting the status to
`kickstart_pending` to avoid incorrect reboot checks
- Skip setting kickstart if the status is already
`kickstart_completed` to prevent unnecessary reboots
- Added cleanup of kickstart files upon VM deletion

* feat(host): download kickstart config from URL

- download content when passing config via URL
- unified boot parameters

* feat(host): copy kickstart kernel files

* feat(host): ensure kickstart completion signal in config

---------

Co-authored-by: happygame <happygame1024@gmail.com>
This commit is contained in:
wanyaoqi
2026-03-20 01:35:27 +08:00
committed by GitHub
parent 9142916763
commit 603f591ebf
11 changed files with 1661 additions and 7 deletions
+4
View File
@@ -133,6 +133,10 @@ func init() {
cmd.BatchPerform("set-root-disk-matcher", &options.ServerSetRootDiskMatcher{})
cmd.Perform("disable-auto-merge-snapshot", &options.ServerDisableAutoMergeSnapshot{})
cmd.BatchPerform("set-tpm", &options.ServerSetTpmOptions{})
cmd.Perform("set-kickstart", &options.ServerKickstartConfigOptions{})
cmd.Perform("delete-kickstart", &options.ServerIdOptions{})
cmd.Perform("kickstart-complete", &options.ServerKickstartCompleteOptions{})
cmd.Get("kickstart", new(options.ServerIdOptions))
cmd.Get("vnc", new(options.ServerVncOptions))
cmd.Get("desc", new(options.ServerIdOptions))
cmd.Get("status", new(options.ServerIdOptions))
+47
View File
@@ -478,6 +478,37 @@ type DeployConfig struct {
Content string `json:"content"`
}
// KickstartConfig Kickstart/Autoinstall自动化安装配置
type KickstartConfig struct {
// 配置文件内容 (当用户直接提供配置时使用)
// required: false
Config string `json:"config,omitempty"`
// 配置文件 URL (当配置文件位于外部服务器时使用)
// required: false
ConfigURL string `json:"config_url,omitempty"`
// 操作系统类型 (用于确定内核参数和文件路径)
// enum: centos,rhel,fedora,openeuler,ubuntu
// required: true
OSType string `json:"os_type" validate:"required,oneof=centos rhel fedora openeuler ubuntu"`
// 是否启用 (用于临时禁用而不删除配置)
// default: true
// required: false
Enabled *bool `json:"enabled,omitempty"`
// 最大重试次数
// default: 3
// required: false
MaxRetries int `json:"max_retries,omitempty"`
// 安装超时时间 (分钟)
// default: 60
// required: false
TimeoutMinutes int `json:"timeout_minutes,omitempty"`
}
type ServerCreateInput struct {
apis.VirtualResourceCreateInput
DeletePreventableCreateInput
@@ -689,9 +720,25 @@ type ServerCreateInput struct {
// 指定用于新建主机的主机镜像ID
GuestImageID string `json:"guest_image_id"`
// Kickstart/Autoinstall自动化安装配置
// required: false
KickstartConfig *KickstartConfig `json:"kickstart_config,omitempty"`
Pod *PodCreateInput `json:"pod"`
}
// ServerUpdateKickstartStatusInput 更新虚拟机 kickstart 状态的输入
type ServerUpdateKickstartStatusInput struct {
// kickstart 状态
// enum: kickstart_pending,kickstart_installing,kickstart_completed,kickstart_failed
// required: true
Status string `json:"status" validate:"required,oneof=kickstart_pending kickstart_installing kickstart_completed kickstart_failed"`
// 错误信息(可选)
// required: false
ErrorMessage string `json:"error_message,omitempty"`
}
func (input *ServerCreateInput) AfterUnmarshal() {
if input.Baremetal {
input.Hypervisor = HYPERVISOR_BAREMETAL
+30
View File
@@ -180,6 +180,12 @@ const (
VM_QGA_SET_NETWORK = "qga_set_network"
VM_QGA_SET_NETWORK_FAILED = "qga_set_network_failed"
// Kickstart自动安装相关状态
VM_KICKSTART_PENDING = "kickstart_pending" // kickstart配置完成,等待VM启动
VM_KICKSTART_INSTALLING = "kickstart_installing" // kickstart正在安装
VM_KICKSTART_COMPLETED = "kickstart_completed" // kickstart安装完成,准备转为running
VM_KICKSTART_FAILED = "kickstart_failed" // kickstart安装失败
SHUTDOWN_STOP = "stop"
SHUTDOWN_TERMINATE = "terminate"
SHUTDOWN_STOP_RELEASE_GPU = "stop_release_gpu"
@@ -274,6 +280,7 @@ const (
var VM_RUNNING_STATUS = []string{VM_START_START, VM_STARTING, VM_RUNNING, VM_BLOCK_STREAM, VM_BLOCK_STREAM_FAIL}
var VM_CREATING_STATUS = []string{VM_CREATE_NETWORK, VM_CREATE_DISK, VM_START_DEPLOY, VM_DEPLOYING}
var VM_KICKSTART_STATUS = []string{VM_KICKSTART_PENDING, VM_KICKSTART_INSTALLING, VM_KICKSTART_COMPLETED, VM_KICKSTART_FAILED}
var HYPERVISORS = []string{
HYPERVISOR_KVM,
@@ -349,8 +356,31 @@ const (
VM_METADATA_CPU_NUMA_PIN_TYPE = "__cpu_numa_pin_type"
VM_METADATA_VIRTUAL_ISOLATED_DEVICE_CONFIG = "__virtual_isolated_device_config"
// Kickstart相关metadata常量
VM_METADATA_KICKSTART_CONFIG = "kickstart_config"
VM_METADATA_KICKSTART_TYPE = "kickstart_type"
VM_METADATA_KICKSTART_ATTEMPT = "kickstart_attempt"
VM_METADATA_KICKSTART_COMPLETED_FLAG = "kickstart_completed_flag"
)
const (
KICKSTART_TYPE_URL = "url"
KICKSTART_TYPE_CONTENT = "content"
)
var KICKSTART_VALID_TYPES = []string{
KICKSTART_TYPE_URL,
KICKSTART_TYPE_CONTENT,
}
var KICKSTART_VALID_OS_TYPES = []string{
"centos",
"rhel",
"fedora",
"ubuntu",
"openeuler",
}
// windows allow a maximal length of 15
// http://support.microsoft.com/kb/909264
const MAX_WINDOWS_COMPUTER_NAME_LENGTH = 15
+1 -1
View File
@@ -719,7 +719,7 @@ func (base *SBaseGuestDriver) RequestUploadGuestStatus(ctx context.Context, gues
}
func (base *SBaseGuestDriver) CanStop(guest *models.SGuest) error {
if utils.IsInStringArray(guest.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.POD_STATUS_CRASH_LOOP_BACK_OFF, api.POD_STATUS_CONTAINER_EXITED}) {
if utils.IsInStringArray(guest.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.POD_STATUS_CRASH_LOOP_BACK_OFF, api.POD_STATUS_CONTAINER_EXITED, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_FAILED, api.VM_KICKSTART_COMPLETED}) {
return nil
}
return errors.Wrapf(errors.ErrInvalidStatus, "Cannot stop server in status %s", guest.Status)
+213 -3
View File
@@ -1202,7 +1202,8 @@ func (self *SGuest) PerformStart(
query jsonutils.JSONObject,
input api.GuestPerformStartInput,
) (jsonutils.JSONObject, error) {
if utils.IsInStringArray(self.Status, []string{api.VM_READY, api.VM_START_FAILED, api.VM_SAVE_DISK_FAILED, api.VM_SUSPEND}) {
validStartStatuses := []string{api.VM_READY, api.VM_START_FAILED, api.VM_SAVE_DISK_FAILED, api.VM_SUSPEND, api.VM_KICKSTART_PENDING, api.VM_KICKSTART_FAILED}
if utils.IsInStringArray(self.Status, validStartStatuses) {
if err := self.ValidateEncryption(ctx, userCred); err != nil {
return nil, errors.Wrap(httperrors.ErrForbidden, "encryption key not accessible")
}
@@ -1429,6 +1430,38 @@ func (self *SGuest) GetDetailsIso(ctx context.Context, userCred mcclient.TokenCr
return desc, nil
}
// 获取Kickstart信息
func (self *SGuest) GetDetailsKickstart(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
result := jsonutils.NewDict()
kickstartConfig, err := self.GetKickstartConfig(ctx, userCred)
if err != nil {
return nil, httperrors.NewInternalServerError("Failed to get kickstart config: %v", err)
}
status := self.GetKickstartStatus(ctx, userCred)
attempt := self.GetMetadata(ctx, api.VM_METADATA_KICKSTART_ATTEMPT, userCred)
if attempt == "" {
attempt = "0"
}
kickstartType := self.GetKickstartType(ctx, userCred)
if kickstartConfig != nil {
configDict := jsonutils.Marshal(kickstartConfig)
result.Set("config", configDict)
} else {
result.Set("config", jsonutils.NewDict())
}
result.Set("status", jsonutils.NewString(status))
result.Set("attempt", jsonutils.NewString(attempt))
result.Set("type", jsonutils.NewString(kickstartType))
return result, nil
}
// 挂载ISO镜像
func (self *SGuest) PerformInsertiso(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if !utils.IsInStringArray(self.Hypervisor, []string{api.HYPERVISOR_KVM, api.HYPERVISOR_BAREMETAL}) {
@@ -3592,7 +3625,7 @@ func (self *SGuest) PerformReset(ctx context.Context, userCred mcclient.TokenCre
return nil, err
}
isHard := jsonutils.QueryBoolean(data, "is_hard", false)
if self.Status == api.VM_RUNNING || self.Status == api.VM_STOP_FAILED {
if utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_FAILED, api.VM_KICKSTART_COMPLETED}) {
drv.StartGuestResetTask(self, ctx, userCred, isHard, "")
return nil, nil
}
@@ -3688,6 +3721,11 @@ func (g *SGuest) SetStatusFromHost(ctx context.Context, userCred mcclient.TokenC
statusStr = api.VM_UNKNOWN
}
}
// Do not override kickstart_installing with running
if statusStr == api.VM_RUNNING && g.Status == api.VM_KICKSTART_INSTALLING {
statusStr = g.Status
log.Infof("guest %s is installing, skip set running status", g.Name)
}
if !hasParentTask {
// migrating status hack
// not change migrating when:
@@ -3778,6 +3816,12 @@ func (self *SGuest) PerformStatus(ctx context.Context, userCred mcclient.TokenCr
}
}
// Do not override kickstart_installing with running
if input.Status == api.VM_RUNNING && self.Status == api.VM_KICKSTART_INSTALLING {
log.Debugf("guest %s is in kickstart_installing state, skip set running status", self.Name)
return nil, nil
}
preStatus := self.Status
_, err := self.SVirtualResourceBase.PerformStatus(ctx, userCred, query, input.PerformStatusInput)
if err != nil {
@@ -3876,7 +3920,7 @@ func (self *SGuest) StartGuestStopAndFreezeTask(ctx context.Context, userCred mc
// 重启
func (self *SGuest) PerformRestart(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
isForce := jsonutils.QueryBoolean(data, "is_force", false)
if utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED}) || (isForce && self.Status == api.VM_STOPPING) {
if utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_FAILED, api.VM_KICKSTART_COMPLETED}) || (isForce && self.Status == api.VM_STOPPING) {
driver, err := self.GetDriver()
if err != nil {
return nil, err
@@ -7165,3 +7209,169 @@ func (self *SGuest) PerformDisableAutoMergeSnapshots(ctx context.Context, userCr
disableAutoMergeSnapshot := jsonutils.QueryBoolean(data, "disable_auto_merge_snapshot", false)
return nil, self.SetMetadata(ctx, api.VM_METADATA_DISABLE_AUTO_MERGE_SNAPSHOT, disableAutoMergeSnapshot, userCred)
}
func (self *SGuest) PerformSetKickstart(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.KickstartConfig) (jsonutils.JSONObject, error) {
// Check if kickstart has already been completed
currentFlag := self.GetMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, userCred)
if currentFlag == "true" {
return jsonutils.Marshal(map[string]string{
"message": "Kickstart has already been completed for this VM",
"status": "failed",
}), nil
}
if err := self.SetKickstartConfig(ctx, &input, userCred); err != nil {
return nil, errors.Wrap(err, "set kickstart config")
}
// Determine and set kickstart type based on input
kickstartType := determineKickstartType(&input)
if err := self.SetKickstartType(ctx, kickstartType, userCred); err != nil {
return nil, errors.Wrap(err, "set kickstart type")
}
if err := self.SetMetadata(ctx, api.VM_METADATA_KICKSTART_ATTEMPT, "0", userCred); err != nil {
return nil, errors.Wrap(err, "set kickstart attempt")
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE, "set kickstart config", userCred)
// Check if VM needs to be restarted
needRestart := false
if utils.IsInStringArray(self.Status, []string{
api.VM_RUNNING,
api.VM_KICKSTART_INSTALLING,
api.VM_KICKSTART_COMPLETED,
api.VM_KICKSTART_FAILED,
api.VM_BLOCK_STREAM,
api.VM_MIGRATING,
}) {
needRestart = true
}
if err := self.SetKickstartStatus(ctx, api.VM_KICKSTART_PENDING, userCred); err != nil {
return nil, errors.Wrap(err, "set kickstart status")
}
// If VM is running, restart it to apply kickstart config
if needRestart {
driver, err := self.GetDriver()
if err != nil {
return nil, errors.Wrap(err, "get driver for restart")
}
if err := driver.StartGuestRestartTask(self, ctx, userCred, false, "kickstart config updated"); err != nil {
return nil, errors.Wrap(err, "start restart task")
}
return jsonutils.Marshal(map[string]string{
"status": "success",
"message": "kickstart config updated, restarting VM",
}), nil
}
return jsonutils.Marshal(map[string]string{
"status": "success",
}), nil
}
func (self *SGuest) PerformKickstartComplete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) {
// Check if kickstart_completed_flag is already set to true
currentFlag := self.GetMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, userCred)
if currentFlag == "true" {
return jsonutils.Marshal(map[string]string{
"status": "success",
"message": "kickstart is already marked as completed",
}), nil
}
// Set the kickstart_completed_flag metadata to true
if err := self.SetMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, "true", userCred); err != nil {
return nil, errors.Wrap(err, "set kickstart completed flag")
}
// Update status to VM_KICKSTART_COMPLETED if in kickstart status
if self.IsInKickstartStatus() && self.Status != api.VM_KICKSTART_COMPLETED {
if err := self.SetStatus(ctx, userCred, api.VM_KICKSTART_COMPLETED, "kickstart manually marked as completed"); err != nil {
return nil, errors.Wrap(err, "update kickstart status")
}
}
// Check if restart is requested (default is true)
restart := jsonutils.QueryBoolean(input, "restart", true)
if restart {
// Trigger VM restart to complete the kickstart process
if utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_COMPLETED, api.VM_KICKSTART_FAILED}) {
driver, err := self.GetDriver()
if err != nil {
return nil, errors.Wrap(err, "get driver")
}
driver.StartGuestRestartTask(self, ctx, userCred, false, "")
return jsonutils.Marshal(map[string]string{
"status": "success",
"message": "kickstart marked as completed, restarting VM",
}), nil
}
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE, "kickstart manually marked as completed", userCred)
return jsonutils.Marshal(map[string]string{
"status": "success",
"message": "kickstart marked as completed",
}), nil
}
func (self *SGuest) PerformDeleteKickstart(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if self.Status == api.VM_KICKSTART_INSTALLING {
return nil, httperrors.NewInvalidStatusError("cannot delete kickstart config while installation is in progress")
}
if err := self.SetKickstartConfig(ctx, nil, userCred); err != nil {
return nil, errors.Wrap(err, "delete kickstart config")
}
// 如果当前是kickstart状态,需要转换回适当的状态
if self.IsInKickstartStatus() {
if self.Status == api.VM_KICKSTART_PENDING {
self.SetStatus(ctx, userCred, api.VM_READY, "kickstart config deleted")
} else if self.Status == api.VM_KICKSTART_COMPLETED {
self.SetStatus(ctx, userCred, api.VM_RUNNING, "kickstart config deleted")
} else if self.Status == api.VM_KICKSTART_FAILED {
self.SetStatus(ctx, userCred, api.VM_READY, "kickstart config deleted")
}
}
// 清理kickstart相关metadata
self.RemoveMetadata(ctx, api.VM_METADATA_KICKSTART_ATTEMPT, userCred)
self.RemoveMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, userCred)
db.OpsLog.LogEvent(self, db.ACT_DELETE, "delete kickstart config", userCred)
return jsonutils.Marshal(map[string]string{
"status": "success",
}), nil
}
func (self *SGuest) PerformUpdateKickstartStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ServerUpdateKickstartStatusInput) (jsonutils.JSONObject, error) {
if err := self.SetKickstartStatus(ctx, input.Status, userCred); err != nil {
return nil, errors.Wrap(err, "update kickstart status")
}
if input.Status == api.VM_KICKSTART_COMPLETED {
if err := self.SetMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, "true", userCred); err != nil {
log.Errorf("Failed to set kickstart completed flag for VM %s: %v", self.Name, err)
}
}
if input.ErrorMessage != "" {
db.OpsLog.LogEvent(self, db.ACT_UPDATE, fmt.Sprintf("kickstart status: %s, error: %s", input.Status, input.ErrorMessage), userCred)
} else {
db.OpsLog.LogEvent(self, db.ACT_UPDATE, fmt.Sprintf("kickstart status: %s", input.Status), userCred)
}
return jsonutils.Marshal(map[string]string{
"status": input.Status,
}), nil
}
+230
View File
@@ -19,7 +19,10 @@ import (
"context"
"database/sql"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@@ -2296,6 +2299,13 @@ func (manager *SGuestManager) validateCreateData(
return nil, httperrors.NewInputParameterError("Invalid userdata: %v", err)
}
// validate KickstartConfig
if input.KickstartConfig != nil {
if err := validateKickstartConfig(input.KickstartConfig); err != nil {
return nil, httperrors.NewInputParameterError("Invalid kickstart config: %v", err)
}
}
err = manager.ValidatePolicyDefinitions(ctx, userCred, ownerId, query, input)
if err != nil {
return nil, err
@@ -2396,6 +2406,125 @@ func (manager *SGuestManager) ValidateCreateData(ctx context.Context, userCred m
return input.JSON(input), nil
}
func validateKickstartConfig(config *api.KickstartConfig) error {
if config.OSType == "" {
return fmt.Errorf("os_type is required")
}
if !utils.IsInStringArray(config.OSType, api.KICKSTART_VALID_OS_TYPES) {
return fmt.Errorf("unsupported os_type: %s, supported types: %v", config.OSType, api.KICKSTART_VALID_OS_TYPES)
}
// 验证配置内容和URL二选一
if config.Config == "" && config.ConfigURL == "" {
return fmt.Errorf("either config or config_url must be provided")
}
if config.Config != "" && config.ConfigURL != "" {
return fmt.Errorf("config and config_url cannot be both provided, choose one")
}
if config.Config != "" {
const maxConfigSize = 64 * 1024
if len(config.Config) > maxConfigSize {
return fmt.Errorf("config content too large: %d bytes, maximum allowed: %d bytes", len(config.Config), maxConfigSize)
}
if len(strings.TrimSpace(config.Config)) == 0 {
return fmt.Errorf("config content cannot be empty")
}
}
if config.ConfigURL != "" {
if len(config.ConfigURL) > 2048 {
return fmt.Errorf("config URL too long: %d characters, maximum allowed: 2048", len(config.ConfigURL))
}
if strings.TrimSpace(config.ConfigURL) == "" {
return fmt.Errorf("config URL cannot be empty")
}
parsedURL, err := url.Parse(config.ConfigURL)
if err != nil {
return fmt.Errorf("invalid URL format: %v", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("invalid URL scheme: %s, only http and https are allowed", parsedURL.Scheme)
}
if parsedURL.Host == "" {
return fmt.Errorf("URL must specify a host")
}
if err := checkKickstartURLContentSize(config.ConfigURL); err != nil {
return fmt.Errorf("URL content validation failed: %v", err)
}
}
// 设置默认值
if config.Enabled == nil {
enabled := true
config.Enabled = &enabled
}
if config.MaxRetries <= 0 {
config.MaxRetries = 3
}
if config.TimeoutMinutes <= 0 {
config.TimeoutMinutes = 60
}
return nil
}
// determineKickstartType determines kickstart type based on config content
func determineKickstartType(config *api.KickstartConfig) string {
if config.Config != "" {
return api.KICKSTART_TYPE_CONTENT
}
return api.KICKSTART_TYPE_URL
}
func checkKickstartURLContentSize(configURL string) error {
const maxURLContentSize = 64 * 1024
const requestTimeout = 10 * time.Second
client := &http.Client{Timeout: requestTimeout}
req, err := http.NewRequest("HEAD", configURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
log.Warningf("Failed to check URL content size for %s: %v", configURL, err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("URL returned status code: %d", resp.StatusCode)
}
contentLengthStr := resp.Header.Get("Content-Length")
if contentLengthStr != "" {
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
if err != nil {
log.Warningf("Failed to parse Content-Length header: %v", err)
return nil
}
if contentLength > maxURLContentSize {
return fmt.Errorf("URL content too large: %d bytes, maximum allowed: %d bytes", contentLength, maxURLContentSize)
}
log.Infof("URL content size validated: %d bytes", contentLength)
} else {
log.Warningf("URL %s does not provide Content-Length header, size validation skipped", configURL)
}
return nil
}
func (manager *SGuestManager) validateEip(ctx context.Context, userCred mcclient.TokenCredential, input *api.ServerCreateInput,
preferRegionId string, preferManagerId string) error {
driver, err := GetDriver(input.Hypervisor, input.Provider)
@@ -2671,6 +2800,34 @@ func (guest *SGuest) PostCreate(ctx context.Context, userCred mcclient.TokenCred
}
}
}
// set kickstart metadata
kickstartConfigJson, _ := data.Get("kickstart_config")
if kickstartConfigJson != nil {
kickstartConfig := &api.KickstartConfig{}
if err := kickstartConfigJson.Unmarshal(kickstartConfig); err != nil {
log.Errorf("unmarshal kickstart config fail: %s", err)
} else {
if err := guest.SetKickstartConfig(ctx, kickstartConfig, userCred); err != nil {
log.Errorf("Failed to set kickstart config for guest %s: %v", guest.Name, err)
} else {
if err := guest.SetKickstartStatus(ctx, api.VM_KICKSTART_PENDING, userCred); err != nil {
log.Errorf("Failed to set kickstart status for guest %s: %v", guest.Name, err)
}
if err := guest.SetMetadata(ctx, api.VM_METADATA_KICKSTART_COMPLETED_FLAG, "false", userCred); err != nil {
log.Errorf("Failed to set kickstart completed flag for guest %s: %v", guest.Name, err)
}
// Determine and set kickstart type based on config
kickstartType := determineKickstartType(kickstartConfig)
if err := guest.SetKickstartType(ctx, kickstartType, userCred); err != nil {
log.Errorf("Failed to set kickstart type for guest %s: %v", guest.Name, err)
}
log.Debugf("Successfully set kickstart config for guest %s with OS type %s", guest.Name, kickstartConfig.OSType)
}
}
}
input := struct {
PreferZone string
@@ -7240,6 +7397,79 @@ func (guest *SGuest) SetGuestBackupMirrorJobInProgress(ctx context.Context, user
return guest.SetMetadata(ctx, api.MIRROR_JOB, api.MIRROR_JOB_INPROGRESS, userCred)
}
func (guest *SGuest) SetKickstartConfig(ctx context.Context, config *api.KickstartConfig, userCred mcclient.TokenCredential) error {
if config == nil {
return guest.RemoveMetadata(ctx, api.VM_METADATA_KICKSTART_CONFIG, userCred)
}
if err := validateKickstartConfig(config); err != nil {
return errors.Wrap(err, "validate kickstart config")
}
configJson := jsonutils.Marshal(config)
return guest.SetMetadata(ctx, api.VM_METADATA_KICKSTART_CONFIG, configJson, userCred)
}
func (guest *SGuest) GetKickstartConfig(ctx context.Context, userCred mcclient.TokenCredential) (*api.KickstartConfig, error) {
configJson := guest.GetMetadataJson(ctx, api.VM_METADATA_KICKSTART_CONFIG, userCred)
if configJson == nil {
return nil, nil
}
config := &api.KickstartConfig{}
if err := configJson.Unmarshal(config); err != nil {
return nil, errors.Wrap(err, "unmarshal kickstart config")
}
return config, nil
}
func (guest *SGuest) SetKickstartStatus(ctx context.Context, status string, userCred mcclient.TokenCredential) error {
if !utils.IsInStringArray(status, api.VM_KICKSTART_STATUS) {
return errors.Errorf("invalid kickstart status: %s", status)
}
return guest.SetStatus(ctx, userCred, status, "")
}
func (guest *SGuest) GetKickstartStatus(ctx context.Context, userCred mcclient.TokenCredential) string {
if utils.IsInStringArray(guest.Status, api.VM_KICKSTART_STATUS) {
return guest.Status
}
return ""
}
func (guest *SGuest) IsInKickstartStatus() bool {
return utils.IsInStringArray(guest.Status, api.VM_KICKSTART_STATUS)
}
func (guest *SGuest) SetKickstartType(ctx context.Context, kickstartType string, userCred mcclient.TokenCredential) error {
if !utils.IsInStringArray(kickstartType, api.KICKSTART_VALID_TYPES) {
return errors.Errorf("invalid kickstart type: %s", kickstartType)
}
return guest.SetMetadata(ctx, api.VM_METADATA_KICKSTART_TYPE, kickstartType, userCred)
}
func (guest *SGuest) GetKickstartType(ctx context.Context, userCred mcclient.TokenCredential) string {
kickstartType := guest.GetMetadata(ctx, api.VM_METADATA_KICKSTART_TYPE, userCred)
if kickstartType == "" {
return api.KICKSTART_TYPE_URL
}
return kickstartType
}
func (guest *SGuest) IsKickstartEnabled(ctx context.Context, userCred mcclient.TokenCredential) bool {
config, err := guest.GetKickstartConfig(ctx, userCred)
if err != nil || config == nil {
return false
}
if config.Enabled == nil {
return true
}
return *config.Enabled
}
func (guest *SGuest) SetGuestBackupMirrorJobNotReady(ctx context.Context, userCred mcclient.TokenCredential) error {
return guest.SetMetadata(ctx, api.MIRROR_JOB, "", userCred)
}
+713
View File
@@ -0,0 +1,713 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package guestman
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
"time"
"gopkg.in/yaml.v3"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/hostman/hostutils"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/mountutils"
)
const (
KICKSTART_MONITOR_TIMEOUT = 60 * time.Minute
SERIAL_FILE_CHECK_INTERVAL = 5 * time.Second
KICKSTART_BASE_DIR = "/tmp/kickstart"
KICKSTART_ISO_MOUNT_DIR = "iso-mount"
KICKSTART_ISO_BUILD_DIR = "iso-build"
KICKSTART_ISO_FILENAME = "config.iso"
REDHAT_KICKSTART_ISO_VOLUME_LABEL = "OEMDRV"
UBUNTU_KICKSTART_ISO_VOLUME_LABEL = "CIDATA"
)
var (
kickstartInstallingRegex = regexp.MustCompile(`(?i).*KICKSTART_INSTALLING.*`)
kickstartCompletedRegex = regexp.MustCompile(`(?i).*KICKSTART_COMPLETED.*`)
kickstartFailedRegex = regexp.MustCompile(`(?i).*KICKSTART_FAILED.*`)
)
// ensureKickstartCompletionSignal ensures that the kickstart config contains the completion signal
// 1. If already contains completion signal, return as is
// 1. If no %post section, append one with completion signal
// 2. If %post exists but no %end, append completion signal before end of file
// 3. If both %post and %end exist, insert completion signal before %end
func ensureKickstartCompletionSignal(config string) string {
completionSignals := []string{
"echo \"KICKSTART_COMPLETED\" > /dev/ttyS1",
"echo 'KICKSTART_COMPLETED' > /dev/ttyS1",
"echo KICKSTART_COMPLETED > /dev/ttyS1",
}
for _, signal := range completionSignals {
if strings.Contains(config, signal) {
return config
}
}
const standardCompletionSignal = "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1"
postIdx := strings.Index(config, "%post")
if postIdx == -1 {
return config + fmt.Sprintf("\n\n%%post\n%s\n%%end", standardCompletionSignal)
}
endIdx := strings.Index(config[postIdx:], "%end")
if endIdx == -1 {
return config + fmt.Sprintf("\n%s\n%%end", standardCompletionSignal)
}
// Find line index of %end within %post section
lines := strings.Split(config, "\n")
absoluteEndIdx := postIdx + endIdx
endLineIdx := -1
currentPos := 0
for i, line := range lines {
lineEndPos := currentPos + len(line) + 1
if currentPos <= absoluteEndIdx && absoluteEndIdx < lineEndPos {
if strings.TrimSpace(line) == "%end" {
endLineIdx = i
break
}
}
currentPos = lineEndPos
}
if endLineIdx != -1 {
newLines := make([]string, len(lines)+1)
copy(newLines, lines[:endLineIdx])
newLines[endLineIdx] = standardCompletionSignal
copy(newLines[endLineIdx+1:], lines[endLineIdx:])
return strings.Join(newLines, "\n")
}
return config
}
// ensureAutoinstallCompletionSignal ensures that the autoinstall config contains the completion signal
// 1. If already contains completion signal, return as is
// 2. If no late-commands section, create one with completion signal
// 3. If late-commands exists but no completion signal, append completion signal
func ensureAutoinstallCompletionSignal(config string) string {
const standardCompletionSignal = "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1"
completionSignals := []string{
"echo \"KICKSTART_COMPLETED\" > /dev/ttyS1",
"echo 'KICKSTART_COMPLETED' > /dev/ttyS1",
"echo KICKSTART_COMPLETED > /dev/ttyS1",
}
// Check if completion signal already exists
for _, signal := range completionSignals {
if strings.Contains(config, signal) {
return config
}
}
// Parse YAML using string keys, use yaml.v3 for better compatibility
var yamlData map[string]interface{}
err := yaml.Unmarshal([]byte(config), &yamlData)
if err != nil {
log.Warningf("Failed to parse autoinstall config as YAML: %v\n", err)
return config
}
autoinstall, ok := yamlData["autoinstall"].(map[string]interface{})
if !ok {
log.Warningf("No autoinstall section found in config\n")
return config
}
lateCommands, exists := autoinstall["late-commands"]
if !exists {
autoinstall["late-commands"] = []interface{}{standardCompletionSignal}
} else {
if commands, ok := lateCommands.([]interface{}); ok {
autoinstall["late-commands"] = append(commands, standardCompletionSignal)
} else {
autoinstall["late-commands"] = []interface{}{standardCompletionSignal}
}
}
result, err := yaml.Marshal(yamlData)
if err != nil {
log.Warningf("Failed to marshal autoinstall config as YAML: %v\n", err)
return config
}
// #cloud-config is required at the top if originally present
return "#cloud-config\n" + string(result)
}
// ensureCompletionSignal ensures that the configuration contains the completion signal based on OS type
func ensureCompletionSignal(osType, config string) string {
switch osType {
case "centos", "rhel", "fedora", "openeuler":
return ensureKickstartCompletionSignal(config)
case "ubuntu":
return ensureAutoinstallCompletionSignal(config)
default:
return config // No validation for unsupported OS types
}
}
type SKickstartSerialMonitor struct {
serverId string
serialFilePath string
ctx context.Context
cancel context.CancelFunc
}
// NewKickstartSerialMonitor creates a new kickstart serial monitor
func NewKickstartSerialMonitor(serverId string) *SKickstartSerialMonitor {
ctx, cancel := context.WithCancel(context.Background())
// Get the server instance to access its homeDir
var serialFilePath string
server, exists := guestManager.GetServer(serverId)
if exists {
serialFilePath = path.Join(server.HomeDir(), "kickstart-serial.log")
} else {
// Fallback to use /tmp if server not found
serialFilePath = fmt.Sprintf("/tmp/kickstart-serial-%s.log", serverId)
}
return &SKickstartSerialMonitor{
serverId: serverId,
serialFilePath: serialFilePath,
ctx: ctx,
cancel: cancel,
}
}
func (m *SKickstartSerialMonitor) GetSerialFilePath() string {
return m.serialFilePath
}
// getKickstartTimeout gets kickstart timeout from kickstart config, defaults to KICKSTART_MONITOR_TIMEOUT
func (m *SKickstartSerialMonitor) getKickstartTimeout() time.Duration {
server, exists := guestManager.GetServer(m.serverId)
if !exists {
return KICKSTART_MONITOR_TIMEOUT
}
kvmGuest, ok := server.(*SKVMGuestInstance)
if !ok {
return KICKSTART_MONITOR_TIMEOUT
}
kickstartConfigStr, exists := kvmGuest.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
if !exists || kickstartConfigStr == "" {
return KICKSTART_MONITOR_TIMEOUT
}
configObj, err := jsonutils.ParseString(kickstartConfigStr)
if err != nil {
return KICKSTART_MONITOR_TIMEOUT
}
var config api.KickstartConfig
if err := configObj.Unmarshal(&config); err != nil {
return KICKSTART_MONITOR_TIMEOUT
}
if config.TimeoutMinutes <= 0 {
return KICKSTART_MONITOR_TIMEOUT
}
timeout := time.Duration(config.TimeoutMinutes) * time.Minute
log.Debugf("Using kickstart timeout %v for server %s", timeout, m.serverId)
return timeout
}
func (m *SKickstartSerialMonitor) ensureSerialFile() error {
if _, err := os.Stat(m.serialFilePath); os.IsNotExist(err) {
file, err := os.Create(m.serialFilePath)
if err != nil {
return errors.Wrapf(err, "create serial file %s", m.serialFilePath)
}
file.Close()
log.Debugf("Created kickstart serial file %s for server %s", m.serialFilePath, m.serverId)
}
return nil
}
func (m *SKickstartSerialMonitor) monitorSerialFile() {
defer func() {
if r := recover(); r != nil {
log.Errorf("KickstartSerialMonitor monitor %v %s", r, debug.Stack())
}
}()
var lastSize int64 = 0
ticker := time.NewTicker(SERIAL_FILE_CHECK_INTERVAL)
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
if err := m.scanSerialForStatus(&lastSize); err != nil {
log.Errorf("Failed to scan serial file for status for server %s: %v", m.serverId, err)
}
}
}
}
// scanSerialForStatus scans the serial file for a kickstart status update.
// It reads new content since the last check, parses it for status keywords,
// and triggers status updates and cleanup when a final status is detected.
func (m *SKickstartSerialMonitor) scanSerialForStatus(lastSize *int64) error {
fileInfo, err := os.Stat(m.serialFilePath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
currentSize := fileInfo.Size()
if currentSize <= *lastSize {
return nil
}
// Read new content
file, err := os.Open(m.serialFilePath)
if err != nil {
return err
}
defer file.Close()
_, err = file.Seek(*lastSize, 0)
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
var status string
var shouldClose bool = false
var matched bool = false
if kickstartInstallingRegex.MatchString(line) {
status = api.VM_KICKSTART_INSTALLING
shouldClose = false
matched = true
} else if kickstartCompletedRegex.MatchString(line) {
status = api.VM_KICKSTART_COMPLETED
shouldClose = true
matched = true
// Unmount ISO and restart VM if kickstart install successfully
if err := m.handleKickstartCompleted(); err != nil {
log.Errorf("Failed to handle kickstart success for server %s: %v", m.serverId, err)
}
} else if kickstartFailedRegex.MatchString(line) {
// TODO: auto retry or alert and stop
status = api.VM_KICKSTART_FAILED
shouldClose = true
matched = true
}
if matched {
log.Infof("Kickstart status update for server %s: %s", m.serverId, status)
if err := m.updateKickstartStatus(status); err != nil {
log.Errorf("Failed to update kickstart status for server %s: %v", m.serverId, err)
} else {
if shouldClose {
m.Close()
return nil
}
}
}
}
*lastSize = currentSize
return scanner.Err()
}
func (m *SKickstartSerialMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
if m.serialFilePath != "" {
if err := os.Remove(m.serialFilePath); err != nil && !os.IsNotExist(err) {
log.Warningf("Failed to remove serial file %s: %v", m.serialFilePath, err)
}
}
log.Debugf("Kickstart monitor closed for server %s", m.serverId)
return nil
}
// updateKickstartStatus updates the kickstart status via Region API
func (m *SKickstartSerialMonitor) updateKickstartStatus(status string) error {
ctx := context.Background()
session := hostutils.GetComputeSession(ctx)
input := api.ServerUpdateKickstartStatusInput{
Status: status,
}
_, err := modules.Servers.PerformAction(session, m.serverId, "update-kickstart-status", jsonutils.Marshal(input))
if err != nil {
return errors.Wrapf(err, "failed to update kickstart status for server %s", m.serverId)
}
return nil
}
// Start starts the kickstart serial monitor
func (m *SKickstartSerialMonitor) Start() error {
log.Debugf("Starting kickstart monitor for server %s, serial file: %s", m.serverId, m.serialFilePath)
if err := m.ensureSerialFile(); err != nil {
return errors.Wrap(err, "ensure serial file")
}
go m.monitorSerialFile()
// Setup timeout handler
go func() {
timeout := m.getKickstartTimeout()
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-m.ctx.Done():
return
case <-timer.C:
log.Warningf("Kickstart monitor timeout (%v) for server %s, setting status to failed", timeout, m.serverId)
if err := m.updateKickstartStatus(api.VM_KICKSTART_FAILED); err != nil {
log.Errorf("Failed to update kickstart status to failed on timeout for server %s: %v", m.serverId, err)
}
m.Close()
}
}()
return nil
}
// handleKickstartCompleted handles the successful kickstart completion
// It unmounts the ISO image and restarts the VM
func (m *SKickstartSerialMonitor) handleKickstartCompleted() error {
// Clean up kickstart files after successful installation
CleanupKickstartFiles(m.serverId)
log.Debugf("Restarting VM %s after successful kickstart", m.serverId)
if err := m.restartServer(); err != nil {
return errors.Wrapf(err, "failed to restart server %s", m.serverId)
}
return nil
}
// restartServer restarts the server using Region API,
// because the kickstart process requires a fully reboot
// to regenerate qemu parameters
func (m *SKickstartSerialMonitor) restartServer() error {
ctx := context.Background()
session := hostutils.GetComputeSession(ctx)
input := jsonutils.NewDict()
input.Set("is_force", jsonutils.JSONFalse)
_, err := modules.Servers.PerformAction(session, m.serverId, "restart", input)
if err != nil {
return errors.Wrapf(err, "failed to restart server %s via API", m.serverId)
}
log.Infof("Server %s restarted after kickstart completion", m.serverId)
return nil
}
// downloadKickstartConfigFromURL downloads kickstart configuration content from the given URL
func downloadKickstartConfigFromURL(configURL string, osType string) (string, error) {
const downloadTimeout = 10 * time.Second
const maxConfigSize = 64 * 1024
// For Ubuntu systems, append "/user-data" to the URL
if osType == "ubuntu" && !strings.HasSuffix(configURL, "/user-data") {
configURL = configURL + "/user-data"
}
client := &http.Client{Timeout: downloadTimeout}
req, err := http.NewRequest("GET", configURL, nil)
if err != nil {
return "", errors.Wrapf(err, "failed to create download request for %s", configURL)
}
resp, err := client.Do(req)
if err != nil {
return "", errors.Wrapf(err, "failed to download config from %s", configURL)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("download failed: URL returned status code: %d", resp.StatusCode)
}
limitedReader := io.LimitReader(resp.Body, int64(maxConfigSize)+1)
content, err := io.ReadAll(limitedReader)
if err != nil {
return "", errors.Wrapf(err, "failed to read config content from %s", configURL)
}
if len(content) > maxConfigSize {
return "", errors.Errorf("downloaded content too large: %d bytes, maximum allowed: %d bytes", len(content), maxConfigSize)
}
if len(strings.TrimSpace(string(content))) == 0 {
return "", errors.Error("downloaded config content is empty")
}
log.Debugf("Successfully downloaded kickstart config from %s: %d bytes", configURL, len(content))
return string(content), nil
}
// CreateKickstartConfigISO creates an ISO image containing kickstart configuration files
// For Red Hat systems: creates ks.cfg in a ISO with label 'OEMDRV'
// For Ubuntu systems: creates user-data and meta-data files in a ISO with label 'CIDATA'
// reference: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/automatically_installing_rhel/starting-kickstart-installations#starting-a-kickstart-installation-automatically-using-a-local-volume
// https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/automatically_installing_rhel/starting-kickstart-installations#starting-a-kickstart-installation-automatically-using-a-local-volume
func CreateKickstartConfigISO(config *api.KickstartConfig, serverId string) (string, error) {
log.Debugf("Creating kickstart ISO for server %s with OS type %s", serverId, config.OSType)
if config == nil {
return "", errors.Errorf("kickstart config is nil")
}
// Download config content from URL if necessary
if config.Config == "" && config.ConfigURL != "" {
log.Debugf("Downloading kickstart config from URL: %s", config.ConfigURL)
downloadedContent, err := downloadKickstartConfigFromURL(config.ConfigURL, config.OSType)
if err != nil {
return "", errors.Wrapf(err, "failed to download kickstart config from URL")
}
config.Config = downloadedContent
log.Debugf("Kickstart config downloaded and set, length: %d", len(config.Config))
}
if config.Config == "" {
return "", errors.Errorf("kickstart config content is empty")
}
// Ensure completion signal is present
validatedConfig := ensureCompletionSignal(config.OSType, config.Config)
// Create temporary directory for ISO contents
tmpDir := filepath.Join(KICKSTART_BASE_DIR, serverId, KICKSTART_ISO_BUILD_DIR)
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return "", errors.Wrapf(err, "failed to create temp directory %s", tmpDir)
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
log.Warningf("Failed to cleanup temp directory %s: %v", tmpDir, err)
}
}()
var filePaths []string
var volumeLabel string
switch config.OSType {
case "centos", "rhel", "fedora", "openeuler":
// Create anaconda-ks.cfg for Red Hat systems
ksFilePath := filepath.Join(tmpDir, "anaconda-ks.cfg")
if err := os.WriteFile(ksFilePath, []byte(validatedConfig), 0644); err != nil {
return "", errors.Wrapf(err, "failed to write kickstart file %s", ksFilePath)
}
filePaths = []string{ksFilePath}
volumeLabel = REDHAT_KICKSTART_ISO_VOLUME_LABEL
case "ubuntu":
// Create user-data file for Ubuntu systems
userDataPath := filepath.Join(tmpDir, "user-data")
if err := os.WriteFile(userDataPath, []byte(validatedConfig), 0644); err != nil {
return "", errors.Wrapf(err, "failed to write user-data file %s", userDataPath)
}
// Create empty meta-data file
metaDataPath := filepath.Join(tmpDir, "meta-data")
if err := os.WriteFile(metaDataPath, []byte(""), 0644); err != nil {
return "", errors.Wrapf(err, "failed to write meta-data file %s", metaDataPath)
}
filePaths = []string{userDataPath, metaDataPath}
volumeLabel = UBUNTU_KICKSTART_ISO_VOLUME_LABEL
default:
return "", errors.Errorf("unsupported OS type: %s", config.OSType)
}
// Create ISO using mkisofs
isoPath := filepath.Join(KICKSTART_BASE_DIR, serverId, KICKSTART_ISO_FILENAME)
args := []string{
"-o", isoPath,
"-V", volumeLabel,
"-r",
"-J",
}
args = append(args, filePaths...)
cmd := exec.Command("mkisofs", args...)
if output, err := cmd.CombinedOutput(); err != nil {
return "", errors.Wrapf(err, "mkisofs failed: %s", string(output))
}
log.Debugf("Successfully created kickstart ISO: %s", isoPath)
return isoPath, nil
}
// ComputeKickstartKernelInitrdPaths returns absolute kernel and initrd paths by combining
// mountPath with OS-specific relative paths and validating that files exist.
// GetKernelInitrdPaths resolves absolute kernel and initrd paths under a mounted ISO.
func GetKernelInitrdPaths(mountPath, osType string) (string, string, error) {
var kernelRelPath, initrdRelPath string
switch osType {
case "centos", "rhel", "fedora", "openeuler":
kernelRelPath = "images/pxeboot/vmlinuz"
initrdRelPath = "images/pxeboot/initrd.img"
case "ubuntu":
kernelRelPath = "casper/vmlinuz"
initrdRelPath = "casper/initrd"
default:
return "", "", errors.Errorf("unsupported OS type: %s", osType)
}
kernelPath := path.Join(mountPath, kernelRelPath)
initrdPath := path.Join(mountPath, initrdRelPath)
if !fileutils2.Exists(kernelPath) {
return "", "", errors.Errorf("kernel file not found: %s", kernelPath)
}
if !fileutils2.Exists(initrdPath) {
return "", "", errors.Errorf("initrd file not found: %s", initrdPath)
}
return kernelPath, initrdPath, nil
}
// BuildKickstartAppendArgs builds kernel append args for kickstart/autoinstall
// based on OS type and whether a local config ISO is present.
// isoPath non-empty indicates a locally attached config ISO.
func BuildKickstartAppendArgs(config *api.KickstartConfig, isoPath string) string {
if config == nil {
return ""
}
baseArgs := []string{}
var kickstartArgs []string
switch config.OSType {
case "centos", "rhel", "fedora", "openeuler":
if isoPath != "" {
kickstartArgs = append(kickstartArgs, fmt.Sprintf("inst.ks=hd:LABEL=%s:/anaconda-ks.cfg", REDHAT_KICKSTART_ISO_VOLUME_LABEL))
} else if config.ConfigURL != "" {
// Fallback to URL directly if no local ISO available
kickstartArgs = append(kickstartArgs, fmt.Sprintf("inst.ks=%s", config.ConfigURL))
} else {
kickstartArgs = append(kickstartArgs, "inst.ks=cdrom:/anaconda-ks.cfg")
}
case "ubuntu":
if isoPath != "" {
kickstartArgs = append(kickstartArgs, "autoinstall")
} else if config.ConfigURL != "" {
// Fallback to URL directly if no local ISO available
kickstartArgs = append(kickstartArgs,
"autoinstall",
"ip=dhcp",
fmt.Sprintf("ds=nocloud-net;s=%s", config.ConfigURL),
)
} else {
kickstartArgs = append(kickstartArgs, "autoinstall", "ip=dhcp", "ds=nocloud;s=/cdrom/")
}
}
return strings.Join(append(baseArgs, kickstartArgs...), " ")
}
// CleanupKickstartFiles cleans up all kickstart-related temporary files and directories
func CleanupKickstartFiles(serverId string) {
if serverId == "" {
log.Warningf("Empty serverId provided for kickstart cleanup")
return
}
kickstartDir := filepath.Join(KICKSTART_BASE_DIR, serverId)
mountPoint := filepath.Join(kickstartDir, KICKSTART_ISO_MOUNT_DIR)
if fileutils2.Exists(mountPoint) {
log.Infof("Unmounting kickstart ISO at %s for server %s", mountPoint, serverId)
if err := mountutils.Unmount(mountPoint, true); err != nil {
log.Errorf("Failed to unmount kickstart ISO at %s: %v", mountPoint, err)
} else {
log.Debugf("Successfully unmounted kickstart ISO at %s for server %s", mountPoint, serverId)
}
}
if server, exists := guestManager.GetServer(serverId); exists {
if kvmGuest, ok := server.(*SKVMGuestInstance); ok && len(kvmGuest.Desc.Cdroms) > 0 {
for _, cdrom := range kvmGuest.Desc.Cdroms {
if cdrom.Path != "" {
filename := path.Base(cdrom.Path)
if strings.HasPrefix(filename, "kickstart-") {
if err := os.Remove(cdrom.Path); err != nil && !os.IsNotExist(err) {
log.Errorf("Failed to cleanup kickstart ISO %s: %v", cdrom.Path, err)
} else {
log.Debugf("Successfully removed kickstart ISO %s for server %s", cdrom.Path, serverId)
}
break
}
}
}
}
}
if fileutils2.Exists(kickstartDir) {
log.Infof("Removing kickstart directory %s for server %s", kickstartDir, serverId)
if err := os.RemoveAll(kickstartDir); err != nil {
log.Errorf("Failed to remove kickstart directory %s: %v", kickstartDir, err)
} else {
log.Debugf("Successfully removed kickstart directory %s for server %s", kickstartDir, serverId)
}
}
log.Debugf("Kickstart files cleanup completed for server %s", serverId)
}
+39 -2
View File
@@ -111,8 +111,9 @@ type SKVMGuestInstance struct {
SKVMInstanceRuntime
*sBaseGuestInstance
Monitor monitor.Monitor
guestAgent *qga.QemuGuestAgent
Monitor monitor.Monitor
guestAgent *qga.QemuGuestAgent
kickstartMonitor *SKickstartSerialMonitor
archMan arch.Arch
}
@@ -1278,6 +1279,8 @@ func (s *SKVMGuestInstance) onMonitorConnected(ctx context.Context) {
s.Monitor.GetVersion(func(v string) {
s.onGetQemuVersion(ctx, v)
})
s.startKickstartMonitorIfNeeded()
}
func (s *SKVMGuestInstance) setDestMigrateTLS(ctx context.Context, data *jsonutils.JSONDict) {
@@ -2092,6 +2095,7 @@ func (s *SKVMGuestInstance) ForceStop() bool {
}
func (s *SKVMGuestInstance) ExitCleanup(clear bool) {
s.cleanupKickstartMonitor()
if clear {
pid := s.GetPid()
if pid > 0 {
@@ -2192,6 +2196,8 @@ func (s *SKVMGuestInstance) delFlatFiles(ctx context.Context) error {
}
func (s *SKVMGuestInstance) Delete(ctx context.Context, migrated, recycle bool) error {
CleanupKickstartFiles(s.Id)
if err := s.delTmpDisks(ctx, migrated); err != nil {
return errors.Wrap(err, "delTmpDisks")
}
@@ -3752,3 +3758,34 @@ func (s *SKVMGuestInstance) HandleGuestStatus(ctx context.Context, resp *api.Hos
}
return resp
}
func (s *SKVMGuestInstance) startKickstartMonitorIfNeeded() {
if s.kickstartMonitor == nil {
return
}
if !s.shouldUseKickstart() {
log.Infof("Kickstart not needed for server %s, skip starting monitor", s.Id)
return
}
log.Infof("Starting kickstart monitor for server %s", s.Id)
if err := s.kickstartMonitor.updateKickstartStatus(api.VM_KICKSTART_INSTALLING); err != nil {
log.Errorf("Failed to update kickstart status to installing for server %s: %v", s.Id, err)
} else {
log.Debugf("Kickstart status updated to installing for server %s", s.Id)
}
if err := s.kickstartMonitor.Start(); err != nil {
log.Errorf("Failed to start kickstart monitor for server %s: %s", s.Id, err)
}
}
func (s *SKVMGuestInstance) cleanupKickstartMonitor() {
if s.kickstartMonitor != nil {
s.kickstartMonitor.Close()
s.kickstartMonitor = nil
log.Infof("Kickstart monitor cleaned up for server %s", s.Id)
}
}
+250 -1
View File
@@ -18,7 +18,9 @@ import (
"context"
"fmt"
"net"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
@@ -44,6 +46,7 @@ import (
"yunion.io/x/onecloud/pkg/hostman/options"
"yunion.io/x/onecloud/pkg/hostman/storageman"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/mountutils"
"yunion.io/x/onecloud/pkg/util/procutils"
"yunion.io/x/onecloud/pkg/util/qemutils"
)
@@ -72,10 +75,12 @@ import sys
import os
import time
import subprocess
import shlex
with open(os.devnull, 'w') as FNULL:
try:
cmd = subprocess.check_output(['bash', '%s'], stderr=FNULL).split()
cmd_str = subprocess.check_output(['bash', '%s'], stderr=FNULL).decode('utf-8').strip()
cmd = shlex.split(cmd_str)
except BaseException as e:
sys.stderr.write('%%s' %% e)
sys.exit(1)
@@ -619,6 +624,11 @@ function start_swtpm() {
input.RescueKernelPath = s.getRescueKernelPath()
}
// check if kickstart is needed for KVM guests
if err := s.configureKickstartBoot(input); err != nil {
return "", errors.Wrap(err, "handle kickstart mount")
}
qemuOpts, err := qemu.GenerateStartOptions(input)
if err != nil {
return "", errors.Wrap(err, "GenerateStartCommand")
@@ -631,6 +641,218 @@ function start_swtpm() {
return cmd, nil
}
// shouldUseKickstart 判断是否需要启用kickstart自动化安装
// 启动kickstart的条件:1. 虚拟机处于KVM虚拟化环境;2. 存在kickstart配置且未禁用;3. kickstart未完成
func (s *SKVMGuestInstance) shouldUseKickstart() bool {
// 只在KVM虚拟化环境下处理kickstart
if s.Desc.Hypervisor != api.HYPERVISOR_KVM {
return false
}
// 检查是否存在kickstart配置
kickstartConfigStr, configExists := s.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
if !configExists || kickstartConfigStr == "" {
return false
} else {
kickstartConfigJson, err := jsonutils.ParseString(kickstartConfigStr)
if err != nil {
log.Errorf("Failed to parse kickstart config for VM %s: %v", s.Id, err)
return false
}
kickstartConfig := &api.KickstartConfig{}
if err := kickstartConfigJson.Unmarshal(kickstartConfig); err != nil {
log.Errorf("Failed to unmarshal kickstart config for VM %s: %v", s.Id, err)
return false
}
if kickstartConfig.Enabled != nil && !*kickstartConfig.Enabled {
log.Debugf("Kickstart is disabled in config for VM %s, skipping kickstart boot", s.Id)
return false
}
}
kickstartCompleted, completedExists := s.Desc.Metadata[api.VM_METADATA_KICKSTART_COMPLETED_FLAG]
if completedExists && kickstartCompleted == "true" {
log.Debugf("Kickstart already completed for VM %s, skipping kickstart boot", s.Id)
return false
}
log.Debugf("VM %s needs kickstart: config exists and not completed yet", s.Id)
return true
}
// configureKickstartBoot 配置 kickstart 自动化安装的启动流程
// 1. 挂载安装 ISO,获取内核和 initrd 路径
// 2. 生成内核启动参数
// 3. 创建 kickstart 监控器
// 4. 如果包含完整配置,生成 kickstart 配置 ISO 并添加为 CDROM 设备
func (s *SKVMGuestInstance) configureKickstartBoot(input *qemu.GenerateStartOptionsInput) error {
if !s.shouldUseKickstart() {
return nil
}
log.Debugf("Enabling kickstart boot for VM %s", s.Id)
kickstartConfigStr := s.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
kickstartConfigJson, err := jsonutils.ParseString(kickstartConfigStr)
if err != nil {
return errors.Wrap(err, "parse kickstart config")
}
kickstartConfig := &api.KickstartConfig{}
if err := kickstartConfigJson.Unmarshal(kickstartConfig); err != nil {
return errors.Wrap(err, "unmarshal kickstart config")
}
// Find ISO file for kickstart installation from CDROM devices
var isoPath string
if len(s.Desc.Cdroms) > 0 {
for _, cdrom := range s.Desc.Cdroms {
if cdrom.Path != "" {
isoPath = cdrom.Path
break
}
}
}
if isoPath == "" {
log.Warningf("no ISO path found for kickstart, skip")
return nil
}
kickstartDir := filepath.Join(KICKSTART_BASE_DIR, s.GetId())
mountPoint := filepath.Join(kickstartDir, KICKSTART_ISO_MOUNT_DIR)
// Check if mount point already exists and is mounted
if fileutils2.Exists(mountPoint) {
mountFile := "/proc/mounts"
if data, err := os.ReadFile(mountFile); err == nil {
lines := strings.Split(string(data), "\n")
mounted := false
for _, line := range lines {
parts := strings.Split(line, " ")
if len(parts) >= 2 && parts[1] == mountPoint {
mounted = true
break
}
}
if mounted {
log.Debugf("Reusing existing kickstart ISO mount at %s for guest %s", mountPoint, s.GetName())
} else {
os.RemoveAll(mountPoint)
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return errors.Wrap(err, "create mount point")
}
if err := mountutils.MountWithParams(isoPath, mountPoint, "iso9660", []string{"-o", "loop,ro"}); err != nil {
os.RemoveAll(mountPoint)
return errors.Wrapf(err, "mount ISO %s to %s", isoPath, mountPoint)
}
log.Debugf("Successfully mounted kickstart ISO %s to %s for guest %s", isoPath, mountPoint, s.GetName())
}
}
} else {
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return errors.Wrap(err, "create mount point")
}
if err := mountutils.MountWithParams(isoPath, mountPoint, "iso9660", []string{"-o", "loop,ro"}); err != nil {
os.RemoveAll(mountPoint)
return errors.Wrapf(err, "mount ISO %s to %s", isoPath, mountPoint)
}
log.Debugf("Successfully mounted kickstart ISO %s to %s for guest %s", isoPath, mountPoint, s.GetName())
}
mountPath := mountPoint
// get kernel and initrd path from mounted ISO
kernelPath, initrdPath, err := GetKernelInitrdPaths(mountPath, kickstartConfig.OSType)
if err != nil {
return errors.Wrap(err, "get kickstart kernel paths")
}
// Copy kernel and initrd files to local directory and unmount ISO
kernelCopyDir := filepath.Join(kickstartDir, "bootfiles")
if err := os.MkdirAll(kernelCopyDir, 0755); err != nil {
return errors.Wrap(err, "create kernel copy directory")
}
kernelFileName := filepath.Base(kernelPath)
initrdFileName := filepath.Base(initrdPath)
copiedKernelPath := filepath.Join(kernelCopyDir, kernelFileName)
copiedInitrdPath := filepath.Join(kernelCopyDir, initrdFileName)
if err := procutils.NewCommand("cp", kernelPath, copiedKernelPath).Run(); err != nil {
return errors.Wrapf(err, "copy kernel from %s to %s", kernelPath, copiedKernelPath)
}
log.Debugf("Successfully copied kernel from %s to %s for guest %s", kernelPath, copiedKernelPath, s.GetName())
if err := procutils.NewCommand("cp", initrdPath, copiedInitrdPath).Run(); err != nil {
return errors.Wrapf(err, "copy initrd from %s to %s", initrdPath, copiedInitrdPath)
}
log.Debugf("Successfully copied initrd from %s to %s for guest %s", initrdPath, copiedInitrdPath, s.GetName())
// Unmount ISO after copying files
log.Infof("Unmounting kickstart ISO at %s after copying kernel and initrd for guest %s", mountPoint, s.GetName())
if err := mountutils.Unmount(mountPoint, true); err != nil {
log.Warningf("Failed to unmount kickstart ISO at %s: %v", mountPoint, err)
} else {
log.Debugf("Successfully unmounted kickstart ISO at %s for guest %s", mountPoint, s.GetName())
// Remove mount point directory after unmounting
if err := os.RemoveAll(mountPoint); err != nil {
log.Warningf("Failed to remove mount point directory %s: %v", mountPoint, err)
}
}
// Use copied file paths instead of mounted paths
kernelPath = copiedKernelPath
initrdPath = copiedInitrdPath
var kickstartConfigIsoPath string
log.Debugf("Kickstart config for guest %s: Config length=%d, ConfigURL=%s",
s.GetName(), len(kickstartConfig.Config), kickstartConfig.ConfigURL)
if kickstartConfig.Config != "" {
isoPath, err := CreateKickstartConfigISO(kickstartConfig, s.Id)
if err != nil {
log.Errorf("Failed to create kickstart config ISO for guest %s: %v, falling back to URL/cdrom method", s.GetName(), err)
} else {
kickstartConfigIsoPath = isoPath
log.Debugf("Successfully created kickstart ISO for guest %s: %s", s.GetName(), isoPath)
}
}
kernelArgs := BuildKickstartAppendArgs(kickstartConfig, kickstartConfigIsoPath)
log.Debugf("Generated kickstart kernel args for guest %s: %s", s.GetName(), kernelArgs)
// Create kickstart serial monitor for status monitoring
kickstartMonitor := NewKickstartSerialMonitor(s.Id)
serialFilePath := kickstartMonitor.GetSerialFilePath()
input.KickstartBoot = &qemu.KickstartBootInfo{
Config: kickstartConfig,
MountPath: mountPath,
KernelPath: kernelPath,
InitrdPath: initrdPath,
KernelArgs: kernelArgs,
SerialFilePath: serialFilePath,
ConfigIsoPath: kickstartConfigIsoPath,
}
// Add kickstart config ISO as additional CDROM device if created
if kickstartConfigIsoPath != "" {
if err := s.attachKickstartISO(kickstartConfigIsoPath); err != nil {
log.Warningf("Failed to attach kickstart config ISO %s: %v", kickstartConfigIsoPath, err)
}
}
s.kickstartMonitor = kickstartMonitor
log.Debugf("Kickstart boot configured for guest %s: kernel=%s, initrd=%s, args=%s, isoPath=%s",
s.GetName(), kernelPath, initrdPath, kernelArgs, kickstartConfigIsoPath)
return nil
}
func (s *SKVMGuestInstance) getRescueInitrdPath() string {
return path.Join(s.GetRescueDirPath(), api.GUEST_RESCUE_INITRAMFS)
}
@@ -1282,3 +1504,30 @@ func (s *SKVMGuestInstance) setUefiBootOrder(ctx context.Context) error {
}
return nil
}
// attachKickstartISO attaches the kickstart ISO as an additional CDROM device
// if the kickstart is not provided by URL
func (s *SKVMGuestInstance) attachKickstartISO(isoPath string) error {
cdromId := fmt.Sprintf("kickstart_iso_%s", s.Id)
log.Debugf("Attaching kickstart ISO %s as CDROM device for guest %s", isoPath, s.GetName())
kickstartCdrom := &desc.SGuestCdrom{
Id: cdromId,
Path: isoPath,
Ordinal: int64(len(s.Desc.Cdroms)),
Scsi: desc.NewScsiDevice("scsi.0", "scsi-cd", fmt.Sprintf("scsi-cd-%s", cdromId)),
DriveOptions: map[string]string{
"readonly": "on",
"media": "cdrom",
"if": "none",
},
}
s.Desc.Cdroms = append(s.Desc.Cdroms, kickstartCdrom)
log.Debugf("Successfully attached kickstart ISO %s as SCSI CDROM device %s (ordinal=%d) for guest %s",
isoPath, cdromId, kickstartCdrom.Ordinal, s.GetName())
return nil
}
+52
View File
@@ -261,6 +261,18 @@ func generateInitrdOptions(drvOpt QemuOptions, initrdPath, kernel string) []stri
return opts
}
func generateKickstartBootOptions(drvOpt QemuOptions, kickstartBoot *KickstartBootInfo) []string {
opts := make([]string, 0)
opts = append(opts, drvOpt.Kernel(kickstartBoot.KernelPath))
opts = append(opts, drvOpt.Initrd(kickstartBoot.InitrdPath))
if kickstartBoot.KernelArgs != "" {
// due to blank space in kickstart args, '' is needed
opts = append(opts, fmt.Sprintf("-append '%s'", kickstartBoot.KernelArgs))
}
return opts
}
func generateDisksOptions(drvOpt QemuOptions, disks []*desc.SGuestDisk, isEncrypt, isMaster bool, osName string) []string {
opts := make([]string, 0)
for _, disk := range disks {
@@ -632,6 +644,25 @@ func generateISASerialOptions(isaSerial *desc.SGuestIsaSerial) []string {
return opts
}
func generateKickstartSerialOptions(kickstartBoot *KickstartBootInfo) []string {
if kickstartBoot == nil || kickstartBoot.SerialFilePath == "" {
return nil
}
opts := make([]string, 0)
chardevId := "kickstart_serial"
// Create chardev with file backend
chardevOpt := fmt.Sprintf("-chardev file,path=%s,id=%s", kickstartBoot.SerialFilePath, chardevId)
opts = append(opts, chardevOpt)
// Create ISA serial device
serialOpt := fmt.Sprintf("-device isa-serial,chardev=%s,id=kickstart_serial_device", chardevId)
opts = append(opts, serialOpt)
return opts
}
func generatePvpanicDeviceOption(pvpanic *desc.SGuestPvpanic) string {
return fmt.Sprintf("-device pvpanic,id=%s,ioport=0x%x", pvpanic.Id, pvpanic.Ioport)
}
@@ -697,6 +728,17 @@ type GenerateStartOptionsInput struct {
RescueInitrdPath string // rescue initramfs path
RescueKernelPath string // rescue kernel path
KickstartBoot *KickstartBootInfo
}
type KickstartBootInfo struct {
Config *api.KickstartConfig
MountPath string
KernelPath string
InitrdPath string
KernelArgs string
SerialFilePath string
ConfigIsoPath string
}
func (input *GenerateStartOptionsInput) HasBootIndex() bool {
@@ -827,6 +869,11 @@ func GenerateStartOptions(
input.RescueInitrdPath,
input.RescueKernelPath,
)...)
} else if input.KickstartBoot != nil {
opts = append(opts, generateKickstartBootOptions(
drvOpt,
input.KickstartBoot,
)...)
}
// generate disk options
@@ -889,6 +936,11 @@ func GenerateStartOptions(
opts = append(opts, generateISASerialOptions(input.GuestDesc.IsaSerial)...)
}
// kickstart serial device
if input.KickstartBoot != nil {
opts = append(opts, generateKickstartSerialOptions(input.KickstartBoot)...)
}
// migrate options
opts = append(opts, getMigrateOptions(drvOpt, input)...)
+82
View File
@@ -493,6 +493,14 @@ type ServerCreateOptionalOptions struct {
AutoPrepaidRecycle bool `help:"automatically enable prepaid recycling after server is created successfully" json:"auto_prepaid_recycle,omitfalse"`
// Kickstart related options
KickstartOSType string `help:"Kickstart OS type" choices:"centos|rhel|fedora|openeuler|ubuntu" json:"-"`
KickstartConfig string `help:"Kickstart configuration content" json:"-"`
KickstartConfigURL string `help:"Kickstart configuration URL" json:"-"`
KickstartEnabled *bool `help:"Enable kickstart" json:"-"`
KickstartMaxRetries int `help:"Kickstart max retries" default:"3" json:"-"`
KickstartTimeoutMinutes int `help:"Kickstart timeout in minutes" default:"60" json:"-"`
GenerateName bool `help:"name is generated by pattern" json:"-"`
EipBw int `help:"allocate EIP with bandwidth in MB when server is created" json:"eip_bw,omitzero"`
@@ -663,6 +671,49 @@ func (opts *ServerCreateOptionalOptions) OptionalParams() (*computeapi.ServerCre
params.IsSystem = &opts.System
// if kickstart os type is specified, then kickstart is enabled
if len(opts.KickstartOSType) > 0 {
if opts.KickstartConfig == "" && opts.KickstartConfigURL == "" {
return nil, fmt.Errorf("either --kickstart-config or --kickstart-config-url must be provided when --kickstart-os-type is specified")
}
if opts.KickstartConfig != "" && opts.KickstartConfigURL != "" {
return nil, fmt.Errorf("--kickstart-config and --kickstart-config-url cannot be both provided, choose one")
}
kickstartConfig := &computeapi.KickstartConfig{
OSType: opts.KickstartOSType,
}
if opts.KickstartConfig != "" {
kickstartConfig.Config = opts.KickstartConfig
}
if opts.KickstartConfigURL != "" {
kickstartConfig.ConfigURL = opts.KickstartConfigURL
}
// setting default value
if opts.KickstartEnabled != nil {
kickstartConfig.Enabled = opts.KickstartEnabled
} else {
enabled := true
kickstartConfig.Enabled = &enabled
}
if opts.KickstartMaxRetries > 0 {
kickstartConfig.MaxRetries = opts.KickstartMaxRetries
} else {
kickstartConfig.MaxRetries = 3
}
if opts.KickstartTimeoutMinutes > 0 {
kickstartConfig.TimeoutMinutes = opts.KickstartTimeoutMinutes
} else {
kickstartConfig.TimeoutMinutes = 60
}
params.KickstartConfig = kickstartConfig
}
return params, nil
}
@@ -896,10 +947,41 @@ func (o *ServerSendKeyOptions) Params() (jsonutils.JSONObject, error) {
return options.StructToParams(o)
}
// Kickstart related options
type ServerKickstartConfigOptions struct {
ServerIdOptions
OSType string `help:"Kickstart OS type" choices:"centos|rhel|fedora|openeuler|ubuntu" json:"os_type"`
Config string `help:"Kickstart configuration content" json:"config,omitempty"`
ConfigURL string `help:"Kickstart configuration URL" json:"config_url,omitempty"`
Enabled *bool `help:"Enable kickstart" json:"enabled,omitempty"`
MaxRetries int `help:"Kickstart max retries" default:"3" json:"max_retries,omitempty"`
TimeoutMinutes int `help:"Kickstart timeout in minutes" default:"60" json:"timeout_minutes,omitempty"`
}
func (o *ServerKickstartConfigOptions) Params() (jsonutils.JSONObject, error) {
if o.Config == "" && o.ConfigURL == "" {
return nil, fmt.Errorf("either --config or --config-url must be provided")
}
if o.Config != "" && o.ConfigURL != "" {
return nil, fmt.Errorf("--config and --config-url cannot be both provided, choose one")
}
return options.StructToParams(o)
}
func (o *ServerSendKeyOptions) Description() string {
return "Send keys to server"
}
type ServerKickstartCompleteOptions struct {
ServerIdOptions
Restart bool `help:"Restart the server after marking kickstart as completed" default:"true" json:"restart"`
}
func (o *ServerKickstartCompleteOptions) Params() (jsonutils.JSONObject, error) {
return options.StructToParams(o)
}
type ServerMonitorOptions struct {
ServerIdOptions