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)",