diff --git a/internal/certmgmt/deployers/sp_huaweiibmc.go b/internal/certmgmt/deployers/sp_huaweiibmc.go new file mode 100644 index 000000000..4029a2ee9 --- /dev/null +++ b/internal/certmgmt/deployers/sp_huaweiibmc.go @@ -0,0 +1,27 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweiibmc" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeHuaweiIBMC, func(options *ProviderFactoryOptions) (core.Deployer, error) { + credentials := domain.AccessConfigForHuaweiIBMC{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate Huawei iBMC access config: %w", err) + } + provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + Host: credentials.Host, + Username: credentials.Username, + Password: credentials.Password, + AllowInsecureConnections: credentials.AllowInsecureConnections, + AutoRestart: xmaps.GetBool(options.ProviderExtendedConfig, "autoRestart"), + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 682277f19..abd9955f9 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -353,6 +353,13 @@ type AccessConfigForHuaweiCloud struct { EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } +type AccessConfigForHuaweiIBMC struct { + Host string `json:"host"` + Username string `json:"username"` + Password string `json:"password"` + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` +} + type AccessConfigForInfomaniak struct { AccessToken string `json:"accessToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index ac6aba95f..26c5d1f5d 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -77,6 +77,7 @@ const ( AccessProviderTypeHostingde = AccessProviderType("hostingde") AccessProviderTypeHostinger = AccessProviderType("hostinger") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") + AccessProviderTypeHuaweiIBMC = AccessProviderType("huaweiibmc") AccessProviderTypeInfomaniak = AccessProviderType("infomaniak") AccessProviderTypeIONOS = AccessProviderType("ionos") AccessProviderTypeJDCloud = AccessProviderType("jdcloud") @@ -396,6 +397,7 @@ const ( DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm") DeploymentProviderTypeHuaweiCloudVOD = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-vod") DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf") + DeploymentProviderTypeHuaweiIBMC = DeploymentProviderType(AccessProviderTypeHuaweiIBMC) DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb") DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn") DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live") diff --git a/pkg/core/deployer/providers/huaweiibmc/huaweiibmc.go b/pkg/core/deployer/providers/huaweiibmc/huaweiibmc.go new file mode 100644 index 000000000..99e46cac3 --- /dev/null +++ b/pkg/core/deployer/providers/huaweiibmc/huaweiibmc.go @@ -0,0 +1,171 @@ +package huaweiibmc + +import ( + "context" + "crypto/rand" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "fmt" + "log/slog" + "net" + "strings" + + "github.com/certimate-go/certimate/pkg/core" + ibmcsdk "github.com/certimate-go/certimate/pkg/sdk3rd/huaweiibmc" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xloop "github.com/certimate-go/certimate/pkg/utils/loop" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // iBMC 主机。 + Host string `json:"host"` + // iBMC 用户名。 + Username string `json:"username"` + // iBMC 密码。 + Password string `json:"password"` + // 是否允许不安全的连接。 + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 是否自动重启。 + AutoRestart bool `json:"autoRestart,omitempty"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger +} + +var _ Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the iBMC deployer is nil") + } + + return &Deployer{ + config: config, + logger: slog.Default(), + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { + // 转换证书格式 + certPFXPwd := make([]byte, 24) + if _, err := rand.Read(certPFXPwd); err != nil { + return nil, fmt.Errorf("failed to generate PFX password: %w", err) + } + certPFXPwdHex := hex.EncodeToString(certPFXPwd) + certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, certPFXPwdHex, nil) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err) + } + + // 创建 iBMC 客户端 + client, err := createSDKClient(d.config.Host, d.config.Username, d.config.Password, d.config.AllowInsecureConnections) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + // 创建会话 + createSessionResp, err := client.CreateSessionWithContext(ctx) + d.logger.Debug("sdk request 'SessionService.CreateSession'", slog.Any("response", createSessionResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'SessionService.CreateSession': %w", err) + } else { + defer client.DeleteSessionWithContext(ctx) + } + + // 查询管理集合资源信息 + listManagersResp, err := client.ListManagersWithContext(ctx) + d.logger.Debug("sdk request 'Managers.ListManagers'", slog.Any("response", listManagersResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'Managers.ListManagers': %w", err) + } + + // 批量更新管理证书 + if len(listManagersResp.Members) == 0 { + d.logger.Info("no ibmc managers to deploy") + } else { + d.logger.Info("found ibmc managers to deploy", slog.Any("managers", listManagersResp.Members)) + + if err := xloop.ForRangeAllWithContext(ctx, listManagersResp.Members, func(ctx context.Context, managerInfo *ibmcsdk.Entity, _ int) error { + importCustomCertificateReq := &ibmcsdk.ImportCustomCertificateToManagerRequest{ + ManagerID: managerInfo.ID, + ManagerLocation: managerInfo.ODataID, + Certificate: base64.StdEncoding.EncodeToString(certPFX), + Password: certPFXPwdHex, + } + importCustomCertificateResp, err := client.ImportCustomCertificateToManagerWithContext(ctx, importCustomCertificateReq) + d.logger.Debug("sdk request 'Managers.SecurityService.HttpsCert.ImportCustomCertificateToManager'", slog.String("params.managerODataId", managerInfo.ODataID), slog.Any("request", importCustomCertificateReq), slog.Any("response", importCustomCertificateResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'Managers.SecurityService.HttpsCert.ImportCustomCertificateToManager': %w", err) + } + + if d.config.AutoRestart { + resetManagerReq := &ibmcsdk.ResetManagerRequest{ + ManagerID: managerInfo.ID, + ManagerLocation: managerInfo.ODataID, + ResetType: "ForceRestart", + } + resetManagerResp, err := client.ResetManagerWithContext(ctx, resetManagerReq) + d.logger.Debug("sdk request 'Managers.ResetManager'", slog.String("params.managerODataId", managerInfo.ODataID), slog.Any("request", resetManagerReq), slog.Any("response", resetManagerResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'Managers.ResetManager': %w", err) + } + } + + return nil + }); err != nil { + return nil, err + } + } + + return &DeployResult{}, nil +} + +func createSDKClient(host, username, password string, skipTlsVerify bool) (*ibmcsdk.Client, error) { + serverUrl := "" + if strings.Contains(host, "://") { + serverUrl = host + } else { + if net.ParseIP(host) != nil && strings.Contains(host, ":") { + host = "[" + host + "]" + } + serverUrl = "https://" + host + } + + client, err := ibmcsdk.NewClient( + serverUrl, + ibmcsdk.WithLogins(username, password), + ) + if err != nil { + return nil, err + } + + if skipTlsVerify { + client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) + } + + return client, nil +} + +func generateCertificatePassword() (string, error) { + password := make([]byte, 24) + if _, err := rand.Read(password); err != nil { + return "", err + } + return hex.EncodeToString(password), nil +} diff --git a/pkg/core/deployer/providers/huaweiibmc/huaweiibmc_test.go b/pkg/core/deployer/providers/huaweiibmc/huaweiibmc_test.go new file mode 100644 index 000000000..bb8c81875 --- /dev/null +++ b/pkg/core/deployer/providers/huaweiibmc/huaweiibmc_test.go @@ -0,0 +1,53 @@ +package huaweiibmc_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweiibmc" + it "github.com/certimate-go/certimate/pkg/core/deployer/testing" +) + +var ( + fp = it.Args("HUAWEIIBMC_") + fTestCertPath string + fTestKeyPath string + fHost string + fUsername string + fPassword string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fHost, "HOST") + fp.DefineString(&fUsername, "USERNAME") + fp.DefineString(&fPassword, "PASSWORD") +} + +/* +Shell command to run this test: + + go test -v ./huaweiibmc_test.go -args \ + --HUAWEIIBMC_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --HUAWEIIBMC_TESTKEYPATH="/path/to/your-test-key.pem" \ + --HUAWEIIBMC_HOST="localhost" \ + --HUAWEIIBMC_USERNAME="admin" \ + --HUAWEIIBMC_PASSWORD="password" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Deploy", func(t *testing.T) { + provider, err := impl.NewDeployer(&impl.DeployerConfig{ + Host: fHost, + Username: fUsername, + Password: fPassword, + AutoRestart: true, + }) + require.NoError(t, err) + + it.TestDeploy(t, provider, it.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/pkg/sdk3rd/huaweiibmc/api_Managers.ListManagers.go b/pkg/sdk3rd/huaweiibmc/api_Managers.ListManagers.go new file mode 100644 index 000000000..51416b7aa --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/api_Managers.ListManagers.go @@ -0,0 +1,53 @@ +package huaweiibmc + +import ( + "context" + "net/http" + "net/url" + "regexp" +) + +type GetManagersResponse struct { + sdkResponseBase + + Members []*Entity `json:"Members"` +} + +type ListManagersResponse struct { + sdkResponseBase + + Members []*Entity `json:"Members"` +} + +func (c *Client) ListManagers() (*ListManagersResponse, error) { + return c.ListManagersWithContext(context.Background()) +} + +func (c *Client) ListManagersWithContext(ctx context.Context) (*ListManagersResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/redfish/v1/Managers") + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &ListManagersResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } else { + for _, member := range result.Members { + if member.ODataID == "" && member.ID != "" { + member.ODataID = "/redfish/v1/Managers/" + url.PathEscape(member.ID) + } + if member.ID == "" && member.ODataID != "" { + re := regexp.MustCompile(`/redfish/v1/Managers/([^/]+)$`) + matches := re.FindStringSubmatch(member.ODataID) + if len(matches) == 2 { + member.ID = matches[1] + } + } + } + } + + return result, nil +} diff --git a/pkg/sdk3rd/huaweiibmc/api_Managers.Reset.go b/pkg/sdk3rd/huaweiibmc/api_Managers.Reset.go new file mode 100644 index 000000000..42a72244d --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/api_Managers.Reset.go @@ -0,0 +1,80 @@ +package huaweiibmc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +type ResetManagerRequest struct { + sdkResponseBase + + ManagerID string `json:"-"` + ManagerLocation string `json:"-"` + ResetType string `json:"ResetType,omitempty"` +} + +type ResetManagerResponse struct { + sdkResponseBase +} + +func (r *ResetManagerResponse) GetAPIError() error { + if r.isSuccessfulResponse() { + return nil + } + + return r.sdkResponseBase.GetAPIError() +} + +func (r *ResetManagerResponse) isSuccessfulResponse() bool { + if r.Error == nil { + return false + } + + var extendedInfo []struct { + MessageID string `json:"MessageId"` + } + if err := json.Unmarshal(r.Error.MessageExtendedInfo, &extendedInfo); err != nil { + return false + } + + for _, info := range extendedInfo { + if info.MessageID == "Base.1.0.Success" { + return true + } + } + + return false +} + +func (c *Client) ResetManager(req *ResetManagerRequest) (*ResetManagerResponse, error) { + return c.ResetManagerWithContext(context.Background(), req) +} + +func (c *Client) ResetManagerWithContext(ctx context.Context, req *ResetManagerRequest) (*ResetManagerResponse, error) { + managerLoc := strings.TrimRight(req.ManagerLocation, "/") + if managerLoc == "" && req.ManagerID != "" { + managerLoc = "/redfish/v1/Managers/" + url.PathEscape(req.ManagerID) + } + if managerLoc == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset managerId") + } + + httpreq, err := c.newRequest(http.MethodPost, managerLoc+"/Actions/Manager.Reset") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ResetManagerResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/huaweiibmc/api_Managers.SecurityService.HttpsCert.ImportCustomCertificate.go b/pkg/sdk3rd/huaweiibmc/api_Managers.SecurityService.HttpsCert.ImportCustomCertificate.go new file mode 100644 index 000000000..7a8866ab7 --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/api_Managers.SecurityService.HttpsCert.ImportCustomCertificate.go @@ -0,0 +1,81 @@ +package huaweiibmc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +type ImportCustomCertificateToManagerRequest struct { + sdkResponseBase + + ManagerID string `json:"-"` + ManagerLocation string `json:"-"` + Certificate string `json:"Certificate,omitempty"` + Password string `json:"Password,omitempty"` +} + +type ImportCustomCertificateToManagerResponse struct { + sdkResponseBase +} + +func (r *ImportCustomCertificateToManagerResponse) GetAPIError() error { + if r.isSuccessfulResponse() { + return nil + } + + return r.sdkResponseBase.GetAPIError() +} + +func (r *ImportCustomCertificateToManagerResponse) isSuccessfulResponse() bool { + if r.Error == nil { + return false + } + + var extendedInfo []struct { + MessageID string `json:"MessageId"` + } + if err := json.Unmarshal(r.Error.MessageExtendedInfo, &extendedInfo); err != nil { + return false + } + + for _, info := range extendedInfo { + if info.MessageID == "iBMC.1.0.CertImportOK" { + return true + } + } + + return false +} + +func (c *Client) ImportCustomCertificateToManager(req *ImportCustomCertificateToManagerRequest) (*ImportCustomCertificateToManagerResponse, error) { + return c.ImportCustomCertificateToManagerWithContext(context.Background(), req) +} + +func (c *Client) ImportCustomCertificateToManagerWithContext(ctx context.Context, req *ImportCustomCertificateToManagerRequest) (*ImportCustomCertificateToManagerResponse, error) { + managerLoc := strings.TrimRight(req.ManagerLocation, "/") + if managerLoc == "" && req.ManagerID != "" { + managerLoc = "/redfish/v1/Managers/" + url.PathEscape(req.ManagerID) + } + if managerLoc == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset managerId") + } + + httpreq, err := c.newRequest(http.MethodPost, managerLoc+"/SecurityService/HttpsCert/Actions/HttpsCert.ImportCustomCertificate") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ImportCustomCertificateToManagerResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/huaweiibmc/api_SessionService.CreateSession.go b/pkg/sdk3rd/huaweiibmc/api_SessionService.CreateSession.go new file mode 100644 index 000000000..ac9cbf078 --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/api_SessionService.CreateSession.go @@ -0,0 +1,60 @@ +package huaweiibmc + +import ( + "context" + "fmt" + "net/http" +) + +type CreateSessionResponse struct { + sdkResponseBase + + XToken string `json:"-"` + XLocation string `json:"-"` +} + +func (c *Client) CreateSession() (*CreateSessionResponse, error) { + return c.CreateSessionWithContext(context.Background()) +} + +func (c *Client) CreateSessionWithContext(ctx context.Context) (*CreateSessionResponse, error) { + c.tokenMu.Lock() + if c.token != "" { + c.tokenMu.Unlock() + return nil, fmt.Errorf("sdkerr: auth error: session already created") + } + + c.tokenMu.Unlock() + + httpreq, err := c.newRequest(http.MethodPost, "/redfish/v1/SessionService/Sessions") + if err != nil { + return nil, err + } else { + httpreq.SetBody(map[string]string{ + "UserName": c.username, + "Password": c.password, + }) + httpreq.SetContext(ctx) + } + + result := &CreateSessionResponse{} + if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } else { + token := httpresp.Header().Get("X-Auth-Token") + location := httpresp.Header().Get("Location") + result.XToken = token + result.XLocation = c.resolveLocation(location) + + if result.XToken == "" { + return result, fmt.Errorf("sdkerr: auth error: received empty token") + } + + c.tokenMu.Lock() + c.token = result.XToken + c.tokenLoc = result.XLocation + c.tokenMu.Unlock() + } + + return result, nil +} diff --git a/pkg/sdk3rd/huaweiibmc/api_SessionService.DeleteSession.go b/pkg/sdk3rd/huaweiibmc/api_SessionService.DeleteSession.go new file mode 100644 index 000000000..28dcaabde --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/api_SessionService.DeleteSession.go @@ -0,0 +1,48 @@ +package huaweiibmc + +import ( + "context" + "fmt" + "net/http" +) + +type DeleteSessionResponse struct { + sdkResponseBase +} + +func (c *Client) DeleteSession() (*DeleteSessionResponse, error) { + return c.DeleteSessionWithContext(context.Background()) +} + +func (c *Client) DeleteSessionWithContext(ctx context.Context) (*DeleteSessionResponse, error) { + c.tokenMu.Lock() + if c.token == "" { + c.tokenMu.Unlock() + return nil, fmt.Errorf("sdkerr: auth error: session not created") + } + if c.tokenLoc == "" { + c.tokenMu.Unlock() + return &DeleteSessionResponse{}, nil + } + + c.tokenMu.Unlock() + + httpreq, err := c.newRequest(http.MethodDelete, c.tokenLoc) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &DeleteSessionResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } else { + c.tokenMu.Lock() + c.token = "" + c.tokenLoc = "" + c.tokenMu.Unlock() + } + + return result, nil +} diff --git a/pkg/sdk3rd/huaweiibmc/client.go b/pkg/sdk3rd/huaweiibmc/client.go new file mode 100644 index 000000000..4caca660a --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/client.go @@ -0,0 +1,156 @@ +// A simple SDK client for Huawei iBMC Redfish. +package huaweiibmc + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + username string + password string + + token string + tokenLoc string + tokenMu sync.Mutex + + rc *resty.Client +} + +type sdkSuccessfulResponse interface { + isSuccessfulResponse() bool +} + +func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { + opts := &Options{} + for _, fn := range optFns { + fn(opts) + } + + if serverUrl == "" { + return nil, fmt.Errorf("sdkerr: unset serverUrl") + } + if _, err := url.Parse(serverUrl); err != nil { + return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) + } + if opts.Username == "" { + return nil, fmt.Errorf("sdkerr: unset username") + } + if opts.Password == "" { + return nil, fmt.Errorf("sdkerr: unset password") + } + + client := &Client{ + username: opts.Username, + password: opts.Password, + } + client.rc = resty.New(). + SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if client.token != "" { + req.Header.Set("X-Auth-Token", client.token) + } + + return nil + }) + + return client, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) SetTLSConfig(config *tls.Config) *Client { + c.rc.SetTLSClientConfig(config) + return c +} + +func (c *Client) newRequest(method string, path string) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.rc.R() + req.Method = method + req.URL = path + + // WARN: + // DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &res) + if successful, ok := res.(sdkSuccessfulResponse); ok && successful.isSuccessfulResponse() { + return resp, nil + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } else { + if err := res.GetAPIError(); err != nil { + return resp, fmt.Errorf("sdkerr: api error: %s", err.Error()) + } + } + } + + return resp, nil +} + +func (c *Client) resolveLocation(location string) string { + u, err := url.Parse(location) + if err != nil || u.IsAbs() { + return location + } + + base, err := url.Parse(c.rc.BaseURL) + if err != nil { + return location + } + + return base.ResolveReference(u).String() +} diff --git a/pkg/sdk3rd/huaweiibmc/models.go b/pkg/sdk3rd/huaweiibmc/models.go new file mode 100644 index 000000000..261cea3e9 --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/models.go @@ -0,0 +1,9 @@ +package huaweiibmc + +type Entity struct { + ID string `json:"Id,omitempty"` + ODataID string `json:"@odata.id,omitempty"` + ODataType string `json:"@odata.type,omitempty"` + Name string `json:"Name,omitempty"` + Description string `json:"Description"` +} diff --git a/pkg/sdk3rd/huaweiibmc/options.go b/pkg/sdk3rd/huaweiibmc/options.go new file mode 100644 index 000000000..e2305d057 --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/options.go @@ -0,0 +1,15 @@ +package huaweiibmc + +type Options struct { + Username string + Password string +} + +type OptionsFunc func(*Options) + +func WithLogins(username, password string) OptionsFunc { + return func(o *Options) { + o.Username = username + o.Password = password + } +} diff --git a/pkg/sdk3rd/huaweiibmc/types.go b/pkg/sdk3rd/huaweiibmc/types.go new file mode 100644 index 000000000..eb930c893 --- /dev/null +++ b/pkg/sdk3rd/huaweiibmc/types.go @@ -0,0 +1,34 @@ +package huaweiibmc + +import ( + "encoding/json" + "fmt" +) + +type sdkResponse interface { + GetAPIError() error +} + +type sdkResponseBase struct { + ODataContext string `json:"@odata.context,omitempty"` + Error *sdkAPIError `json:"error,omitempty"` +} + +type sdkAPIError struct { + Code string `json:"code"` + Message string `json:"message"` + MessageExtendedInfo json.RawMessage `json:"@Message.ExtendedInfo,omitempty"` +} + +func (e sdkAPIError) Error() string { + return fmt.Sprintf("[%s] %s", e.Code, e.Message) +} + +func (r *sdkResponseBase) GetAPIError() error { + if r.Error != nil { + return *r.Error + } + return nil +} + +var _ sdkResponse = (*sdkResponseBase)(nil) diff --git a/ui/public/imgs/providers/huawei.svg b/ui/public/imgs/providers/huawei.svg new file mode 100644 index 000000000..acd9f3182 --- /dev/null +++ b/ui/public/imgs/providers/huawei.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 2506154d4..982977e52 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -64,6 +64,7 @@ import AccessConfigFieldsProviderHetzner from "./AccessConfigFieldsProviderHetzn import AccessConfigFieldsProviderHostingde from "./AccessConfigFieldsProviderHostingde"; import AccessConfigFieldsProviderHostinger from "./AccessConfigFieldsProviderHostinger"; import AccessConfigFieldsProviderHuaweiCloud from "./AccessConfigFieldsProviderHuaweiCloud"; +import AccessConfigFieldsProviderHuaweiIBMC from "./AccessConfigFieldsProviderHuaweiIBMC"; import AccessConfigFieldsProviderInfomaniak from "./AccessConfigFieldsProviderInfomaniak"; import AccessConfigFieldsProviderIONOS from "./AccessConfigFieldsProviderIONOS"; import AccessConfigFieldsProviderJDCloud from "./AccessConfigFieldsProviderJDCloud"; @@ -193,6 +194,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }) }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + host: "", + username: "", + password: "", + allowInsecureConnections: true, + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + host: z.string().refine((v) => isHostname(v) || isUrlWithHttpOrHttps(v), t("common.errmsg.host_invalid")), + username: z.string().nonempty(), + password: z.string().nonempty(), + allowInsecureConnections: z.boolean().nullish(), + }); +}; + +export default Object.assign(AccessConfigFieldsProviderHuaweiIBMC, { getInitialValues, getSchema }); diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index 9c1d4e157..958e4d764 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -74,6 +74,7 @@ import BizDeployNodeConfigFieldsProviderHuaweiCloudLive from "./BizDeployNodeCon import BizDeployNodeConfigFieldsProviderHuaweiCloudOBS from "./BizDeployNodeConfigFieldsProviderHuaweiCloudOBS"; import BizDeployNodeConfigFieldsProviderHuaweiCloudVOD from "./BizDeployNodeConfigFieldsProviderHuaweiCloudVOD"; import BizDeployNodeConfigFieldsProviderHuaweiCloudWAF from "./BizDeployNodeConfigFieldsProviderHuaweiCloudWAF"; +import BizDeployNodeConfigFieldsProviderHuaweiIBMC from "./BizDeployNodeConfigFieldsProviderHuaweiIBMC"; import BizDeployNodeConfigFieldsProviderJDCloudALB from "./BizDeployNodeConfigFieldsProviderJDCloudALB"; import BizDeployNodeConfigFieldsProviderJDCloudCDN from "./BizDeployNodeConfigFieldsProviderJDCloudCDN"; import BizDeployNodeConfigFieldsProviderJDCloudLive from "./BizDeployNodeConfigFieldsProviderJDCloudLive"; @@ -226,6 +227,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }) }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + autoRestart: true, + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + autoRestart: z.boolean().nullish(), + }); +}; + +export default Object.assign(BizDeployNodeConfigFieldsProviderHuaweiIBMC, { getInitialValues, getSchema }); diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 1b0861d25..a725f2097 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -79,6 +79,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ HOSTINGDE: "hostingde", HOSTINGER: "hostinger", HUAWEICLOUD: "huaweicloud", + HUAWEIIBMC: "huaweiibmc", INFOMANIAK: "infomaniak", IONOS: "ionos", JDCLOUD: "jdcloud", @@ -230,6 +231,7 @@ export const accessProvidersMap: Map ).map(([type, name, category, builtin]) => [ type, diff --git a/ui/src/i18n/resources/en/nls.access.json b/ui/src/i18n/resources/en/nls.access.json index a14aafeee..79f66b93e 100644 --- a/ui/src/i18n/resources/en/nls.access.json +++ b/ui/src/i18n/resources/en/nls.access.json @@ -796,6 +796,18 @@ "placeholder": "Please enter Huawei Cloud enterprise project ID", "tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-em/em_03_0000.html" }, + "huaweiibmc_host": { + "label": "iBMC Redfish server host", + "placeholder": "Please enter iBMC Redfish server host" + }, + "huaweiibmc_username": { + "label": "iBMC username", + "placeholder": "Please enter the iBMC username" + }, + "huaweiibmc_password": { + "label": "iBMC password", + "placeholder": "Please enter the iBMC password" + }, "infomaniak_access_token": { "label": "Infomaniak access token", "placeholder": "Please enter Infomaniak access token", diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index 6bde52d08..94a5cd8e2 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -171,6 +171,7 @@ "huaweicloud_scm_upload": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)", "huaweicloud_vod": "Huawei Cloud - VOD (Video on Demand)", "huaweicloud_waf": "Huawei Cloud - WAF (Web Application Firewall)", + "huaweiibmc": "Huawei iBMC", "infomaniak": "Infomaniak", "ionos": "IONOS", "jdcloud": "JD Cloud", diff --git a/ui/src/i18n/resources/en/nls.workflow.nodes.json b/ui/src/i18n/resources/en/nls.workflow.nodes.json index f6799ec9d..92c9c789c 100644 --- a/ui/src/i18n/resources/en/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/en/nls.workflow.nodes.json @@ -1717,6 +1717,9 @@ "placeholder": "Please enter Huawei Cloud WAF certificate ID", "tooltip": "For more information, see https://console-intl.huaweicloud.com/console/#/waf/certificateManagement" }, + "huaweiibmc_auto_restart": { + "label": "Auto restart iBMC after deployment" + }, "jdcloud_alb_region_id": { "label": "JD Cloud region ID", "placeholder": "Please enter JD Cloud ALB region ID (e.g. cn-north-1)", diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json index 090557e15..7c2f3a72c 100644 --- a/ui/src/i18n/resources/zh/nls.access.json +++ b/ui/src/i18n/resources/zh/nls.access.json @@ -796,6 +796,18 @@ "placeholder": "请输入华为云企业项目 ID", "tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-em/zh-cn_topic_0126101490.html" }, + "huaweiibmc_host": { + "label": "iBMC Redfish 服务器地址", + "placeholder": "请输入 iBMC Redfish 服务器地址" + }, + "huaweiibmc_username": { + "label": "iBMC 用户名", + "placeholder": "请输入 iBMC 用户名" + }, + "huaweiibmc_password": { + "label": "iBMC 密码", + "placeholder": "请输入 iBMC 密码" + }, "infomaniak_access_token": { "label": "Infomaniak AccessToken", "placeholder": "请输入 Infomaniak AccessToken", diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 517c82329..7c9437a71 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -171,6 +171,7 @@ "huaweicloud_scm_upload": "华为云 - 上传到云证书管理服务 SCM", "huaweicloud_vod": "华为云 - 视频点播 VOD", "huaweicloud_waf": "华为云 - Web 应用防火墙 WAF", + "huaweiibmc": "华为 iBMC", "infomaniak": "Infomaniak", "ionos": "IONOS", "jdcloud": "京东云", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index a181213c9..1dd59940f 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -1716,6 +1716,9 @@ "placeholder": "请输入华为云 WAF 证书 ID", "tooltip": "这是什么?请参阅 https://console.huaweicloud.com/console/#/waf/certificateManagement" }, + "huaweiibmc_auto_restart": { + "label": "部署后自动重启 iBMC 服务" + }, "jdcloud_alb_region_id": { "label": "京东云服务地域 ID", "placeholder": "请输入京东云 ALB 服务地域 ID(例如:cn-north-1)",