zoukankan      html  css  js  c++  java
  • WinCC的电子签名与审计追踪 2.0

    之前写过一篇WinCC的电子签名与审计追踪,在那篇文章中使用报警操作记录生成审计追踪,后来测试VB脚本执行的情况,发现审计追踪中缺少执行该操作的用户名和计算机名,用C脚本执行倒是没有问题。在本文中再补充一个用InserAuditEntryNew生成审计追踪的方法,并且不再把电子签名和审计追踪做在一个函数里,将电子签名和审计追踪分成两个函数分别执行可以更灵活。

     目录

    生成审计追踪的方法


     用脚本向Audit中添加记录有两种方法,一种方法是用WinCC提供的InserAuditEntryNew函数写入,另一种方法是生成属于“操作员输入消息”类型的报警消息,该报警消息同时也会记录到Audit中。

    WinCC中的某些操作本身就会生成审计追踪记录,例如启动或关闭系统、登录用户等,这些操作生成的Audit记录的TargetName列是操作内容,而使用InserAuditEntryNew函数生成的记录在TargetName列的内容默认为VBScripting Runtime或CScripting Runtime,而操作内容只能记录在Reason列处。

    WinCC的电子签名与审计追踪一文中介绍了如何用“操作员输入消息”类型的报警生成把操作内容记录在TargetName列的审计追踪消息,之前以C脚本测试过这个方法可以达到要求,后来用VB脚本测试,审计追踪中ApplicationUser和ComputerName中是空的,即没有操作人和操作终端名称。如果要用VB脚本执行,这种方法是不能满足审计追踪要求的。在本文中再介绍使用InserAuditEntryNew函数生成审计追踪的方法。

    VB脚本的电子签名和审计追踪


    VB脚本的InsertAuditEntryNew函数

    函数原型:

    InsertAuditEntryNew(strOldValue, strNewValue, strOpComments, iComment)
    

    参数:

    参数 描述
    strOldValue 旧值
    strNewValue 新值
    strOpComments 注释
    iComment

    0:将strOpComments参数的值作为注释记录到audit trial中

    1:显示一个对话框,将对话框中输入的内容的作为注释记录到audit trial中

    返回值:

    • 整型值,含义未知,没有找到对此说明的官方文档。

    VB脚本的审计追踪

    把InserAuditEntryNew函数再封装为CreateOpMsg,添加一些参数。在VB全局脚本的项目模块中建立该函数。

     函数原型:

    Function CreateOpMsg(sSource, sInputMsg, OldValue, NewValue, sComments)
    

    代码:

    '----------------------------------------------------------------------------
    ' 描述:
    '   该函数向audit数据库中插入一条审计追踪记录,
    '   在数据库的reason列写入"[sSource] sInputMsg (sComments)"字符串,
    '   由三个字符串参数拼接而成。
    ' 参数:
    '   sSource    :该字符串表示这条记录的来源
    '   sInputMsg  :该字符串表示这条记录的操作说明
    '   OldValue   :旧值
    '   NewValue   :新值
    '   sComments  :用户注释
    ' 返回值:
    '   与InsertAuditEntryNew函数返回值相同。
    '----------------------------------------------------------------------------
    Function CreateOpMsg(sSource, sInputMsg, OldValue, NewValue, sComments)
        Dim strOpComments
        strOpComments = ""
            
        If sSource <> "" Then
        	strOpComments = strOpComments & "[" & sSource & "] "
        End If 
            
        strOpComments = strOpComments & sInputMsg
            
        If Trim(sComments) <> "" Then
        	strOpComments = strOpComments & " (" & sComments & ")"
        End If 
            
        CreateOpMsg = InsertAuditEntryNew(CStr(OldValue), CStr(NewValue), strOpComments, 0)
    '--------------------------------
    ' 以下为通过报警生成审计追踪的代码
    '--------------------------------
    '   '创建操作员消息
    '    dim myAlarm
    '    Set myAlarm = HMIRuntime.Alarms(12508142) 
    '    MyAlarm.State = 5
    '    '--------------------------
    '    'State  Alarm Log Status
    '    '1      Came In
    '    '2      Went Out
    '    '5      Came in and comment
    '    '6      Gone and comment
    '    '--------------------------
    '    myAlarm.Comment = sComments
    '    myAlarm.UserName = HMIRuntime.Tags("@NOP::@CurrentUser").Read
    '    myAlarm.ProcessValues(2) = OldValue
    '    myAlarm.ProcessValues(3) = NewValue
    '    myAlarm.ProcessValues(10) = sSource
    '    myAlarm.ProcessValues(7) = sInputMsg
    '    MyAlarm.Create
    End Function
    

     

    VB脚本的电子签名

    在VB全局脚本的项目模块中建立以下函数。

    函数原型:

    Function EsigDialog(Byref sComments, Byval bForcecomment, Byval sUserName)
    

    代码:

    '-----------------------------------------------------------------------------------------
    ' 描述:
    '   执行该函数将弹出电子签名对话框,
    '   并要求输入注释,可以设置是否要求强制注释,
    '   注释内容通过sComments参数返回,
    '   如果未给sUserName参数写入字符串,则使用当前登录的账户执行电子签名,
    '   如果给sUserName参数写入了字符串,则将该字符串作为用户名执行电子签名。
    ' 参数:
    '   sComments     :引用一个字符串变量,注释内容将会写入到该变量中。
    '   bForcecomment :
    '                   False:可以不输入注释,即注释为空;
    '                   True :必须输入注释。
    '   sUserName     :执行电子签名的用户名,如果该参数为空字符串,则使用当前登录的账户执行电子签名。
    ' 返回值:
    '   -1:函数执行遇到错误
    '    1:电子签名通过
    '    2:用户取消了电子签名
    '    3:三次电子签名未通过
    '------------------------------------------------------------------------------------------
    Function EsigDialog(Byref sComments, Byval bForcecomment, Byval sUserName)
        Dim sDisplayedUser, sDomain
        sDomain = ""  '如果加入了域,在此填写域名
        '如果sUserName参数不为空,则使用提供的用户名做电子签名,否则使用当前登录的账户做电子签名
        sUserName = Trim(sUserName)
        if sUserName <> "" then  
            sDisplayedUser = sUserName
        Else      
            sUserName      = HMIRuntime.Tags("@NOP::@CurrentUser").Read
            sDisplayedUser = HMIRuntime.Tags("@NOP::@CurrentUserName").Read
            If sUserName = "" Then
                Select Case HMIRuntime.Language
                    Case 2052
                        Msgbox "用户未登录,请登录后再执行!"
                    Case 1033
                        Msgbox "Please log in before executing!"
                    Case Else
                        Msgbox "Please log in before executing!"
                End Select
                EsigDialog = -1
                Exit Function
            End If
        End If
    
        '电子签名
        Dim myEsig
        Dim ret
        Set myEsig = CreateObject("CCEsigDlg.ESIG")
        '强制注释
        myEsig.forcecomment = bForcecomment
        ret = myEsig.showDialog(sUserName,sDisplayedUser,sDomain, HMIRuntime.Language, sComments)
        EsigDialog = ret
    End Function
    

    VB脚本的电子签名和审计追踪示例

    '---------------------------------------------------------------
    ' 设定一个变量的新值,完成电子签名后,将修改的值记录到审计追踪
    '---------------------------------------------------------------'
    Dim OldValue, NewValue
    Dim sComments
    NewValue = inputbox("设定加热温度")
    if Not IsNumeric(NewValue) then
        msgbox ("只能输入数字!")
    else
        if EsigDialog(sComments, False, "") = 1 then
            OldValue = HMIRuntime.Tags("tag1").Read
            HMIRuntime.Tags("tag1").Write NewValue
            Call CreateOpMsg("<设备名称>", "设定加热温度" , OldValue, NewValue, sComments)
        end if
    end if
    

      

    VB脚本的写入变量新值的封装函数

    在通过电子签名后,向一个变量写入新值,并记录到审计追踪,这个操作在WinCC中经常会使用。可以看到前面的代码稍有复杂,对这个过程再做一次封装在使用时会更加方便。

    函数原型:

    Function TagNewValueES(sSource, sInputMsg, sTagName, NewValue)
    

    代码:

    '------------------------------------------------------------------------------------
    ' 描述:
    '   执行电子签名,通过后向一个变量中写入新值,并将变量的新值和旧值记录到审计追踪,
    '   在审计追踪的reason列中写入"[sSource] sTagName: sInputMsg (sComments)"字符串。
    ' 参数:
    '   sSource    :该字符串表示这条记录的来源
    '   sInputMsg  :该字符串表示修改变量的操作说明
    '   sTagName   :变量名
    '   NewValue   :新值
    ' 返回值:
    '   -1:函数执行遇到错误
    '    1:电子签名通过
    '    2:用户取消了电子签名
    '    3:三次电子签名未通过
    '--------------------------------------------------------------------------------------
    
    Function TagNewValueES(sSource, sInputMsg, sTagName, NewValue)
        Dim OldValue
        Dim sComments
        Dim iRet
        iRet = EsigDialog(sComments, False, "")
        if iRet = 1 then
            OldValue = HMIRuntime.Tags( sTagName ).Read
            HMIRuntime.Tags( sTagName ).Write NewValue
            Call CreateOpMsg(sSource, sTagName&": "&sInputMsg , OldValue, NewValue, sComments)
        end if
        TagNewValueES = iRet
    End Function
    

      

    C脚本的电子签名和审计追踪


    C脚本的InsertAuditEntryNew函数

    函数原型:

    long int InsertAuditEntryNew(char* szOldValue, char* szNewValue, char* szOpComments, BOOL iComment, char* szReturnBuffer)
    

    参数:

    参数 描述
    szOldValue 旧值
    szNewValue 新值
    szOpComments 注释
    iComment

    0:将strOpComments参数的值作为注释记录到audit trial中

    1:显示一个对话框,将对话框中输入的内容的作为注释记录到audit trial中

    szReturnBuffer

    引用一个字符串数组,含义不明,官方文档未对此说明

    返回值:

    • 整型值,含义未知,没有找到对此说明的官方文档。

    C脚本的审计追踪

    把InserAuditEntryNew函数再封装为CreateOpMsg,添加一些参数。在C全局脚本的项目函数中建立该函数。

    函数原型:

    long int CreateOpMsg(char* szSource, char* szMsg, char* szOldValue, char* szNewValue, char *szComments)
    

    代码:

    #include "apdefap.h"
    /*---------------------------------------------------------------------------------------------------
    * 描述:
    *   该函数向audit数据库中插入一条审计追踪记录,
    *   在数据库的reason列写入"[szSource] szMsg (szComments)"字符串,
    *   由三个字符串参数拼接而成。
    * 参数:
    *   szSource    :该字符串表示这条记录的来源
    *   szMsg       :该字符串表示这条记录的操作说明
    *   szOldValue  :旧值
    *   szNewValue  :新值
    *   szComments  :用户注释
    * 返回值:
    *   与InsertAuditEntryNew函数返回值相同。
    *----------------------------------------------------------------------------------------------------*/
    long int CreateOpMsg(char* szSource, char* szMsg, char* szOldValue, char* szNewValue, char *szComments)
    {
        char szCommentsBuffer[1024] = "";
        char szBuffer[1024] = "";
        char szReturnBuffer[1024] = "";
    
        if (szSource != NULL && strlen(szSource) > 0) {
            sprintf(szBuffer,"[%s] ", szSource);
            strcat(szCommentsBuffer, szBuffer);
        }
    
        strcat(szCommentsBuffer, szMsg);
    
        if (szComments != NULL && strlen(szComments) > 0) {
            sprintf(szBuffer," (%s)", szComments);
            strcat(szCommentsBuffer, szBuffer);
        }
    
        return InsertAuditEntryNew(szOldValue,szNewValue,szCommentsBuffer,0,szReturnBuffer);    //Return-Type: long int
    }
    

      

    C脚本的电子签名

    在C全局脚本的项目函数中建立以下函数。

    函数原型:

    int EsigDialog(char* szComments,BOOL bForcecomment, char* szUserName)

    代码:

    #include "apdefap.h"
    /*---------------------------------------------------------------------------------------------
    * 描述:
    *   执行该函数将弹出电子签名对话框,
    *   并要求输入注释,可以设置是否要求强制注释,
    *   注释内容通过szComments参数返回,
    *   如果未给szUserName参数写入字符串,则使用当前登录的账户执行电子签名,
    *   如果给szUserName参数写入了字符串,则将该字符串作为用户名执行电子签名。
    * 参数:
    *   szComments     :引用一个字符串变量,注释内容将会写入到该变量中。
    *   bForcecomment  :
    *                    False:可以不输入注释,即注释为空;
    *                    True :必须输入注释。
    *   szUserName     :执行电子签名的用户名,如果该参数为空字符串,则使用当前登录的账户执行电子签名。
    * 返回值:
    *   -1:函数执行遇到错误
    *    1:电子签名通过
    *    2:用户取消了电子签名
    *    3:三次电子签名未通过
    *-----------------------------------------------------------------------------------------------*/
    int EsigDialog(char* szComments,BOOL bForcecomment, char* szUserName)
    {
        char *Domain = "";  //如果加入了域,在此填写域名
        char *szDisplayedUser = "";
        int iRet = 0;
        VARIANT vtComment;
        __object* EsigDlg;
        char szUserNameBuffer[256]="";
        char *strBegin = NULL;
        char *strEnd   = NULL;
    
        /* 删除szUserName字符串两端空格 */
        if (szUserName != NULL ){
            strBegin = szUserName;
            while(*strBegin && isspace(*strBegin))
                strBegin++; //如果是空格,首地址往前移一位,如果不是,则跳过该循环
            strncpy(szUserNameBuffer,strBegin,255);
            strEnd = szUserNameBuffer + (strlen(szUserNameBuffer) - 1);
            while(*strEnd && isspace(*strEnd))
                *strEnd-- = ''; //如果是空格,末地址往前移一位,并赋结束符
        }
        /* 如果szUserNameBuffer参数不为空,则使用提供的用户名做电子签名,否则使用当前登录的账户做电子签名 */
        if (strlen(szUserNameBuffer)>0) {
            szUserName = szUserNameBuffer;
            szDisplayedUser = szUserName;
        } else {
            szUserName      = GetTagChar("@NOP::@CurrentUser");  //Return-Type: char* 
            szDisplayedUser = GetTagChar("@NOP::@CurrentUserName");  //Return-Type: char* 
            /* 判断用户是否登录 */
            if (strlen(szUserName) == 0) {
                switch (GetLanguage()) {
                    case 2052:
                        MessageBox(NULL,"用户未登录,请登录后再执行!","Error",MB_SYSTEMMODAL|MB_OK);
                        break;
                    case 1033:
                    default:
                        MessageBox(NULL,"Please log in before executing!","Error",MB_SYSTEMMODAL|MB_OK);
                        break;
                } 
                return -1;
            }
        }
        
        /* 电子签名 */
        EsigDlg = __object_create("CCESigDlg.ESIG");
     
        if (!EsigDlg) {
            printf("Failed to create Picture Object
    ");
            return -1;
        }
     
        EsigDlg->forcecomment = bForcecomment; 
        iRet = EsigDlg->ShowDialog(szUserName,szDisplayedUser,Domain,GetLanguage(),&vtComment);
        __object_delete(EsigDlg);
        /* 提取注释 */
        if (szComments != NULL)
            sprintf(szComments,"%ls",vtComment.u.bstrVal);
        VariantClear(&vtComment);
        
        return iRet;
    }
    

      

    C脚本的电子签名和审计追踪示例

    #include "apdefap.h"
    void OnClick(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName)
    {
    
    // WINCC:TAGNAME_SECTION_START
    // syntax: #define TagNameInAction "DMTagName"
    // next TagID : 1
    // WINCC:TAGNAME_SECTION_END
    
    // WINCC:PICNAME_SECTION_START
    // syntax: #define PicNameInAction "PictureName"
    // next PicID : 1
    // WINCC:PICNAME_SECTION_END
    
    /*------------------------------------------------------------------
    * 设定一个变量的新值,完成电子签名后,将修改的值记录到审计追踪
    *------------------------------------------------------------------*/
        char szComments[2014]="";
        double OldValue, NewValue;
        char sOldValue[512]="";
        char sNewValue[512]="";
        NewValue = 97;
        if (EsigDialog (szComments, FALSE, "") == 1 ) {
            OldValue = GetTagDouble("tag1");
            SetTagDouble("tag1", NewValue);
            sprintf(sOldValue,"%f",OldValue);
            sprintf(sNewValue,"%f",NewValue);
            CreateOpMsg("<设备名称>", "设定加热温度", sOldValue, sNewValue, szComments);
        }
    }
    

      

    C脚本的写入变量新值的封装函数

    在通过电子签名后,向一个变量写入新值,并记录到审计追踪,这个操作在WinCC中经常会使用。可以看到前面的代码稍有复杂,对这个过程再做一次封装在使用时会更加方便。

    向数值类型的变量写入新值

    函数原型:

    int DoubleTagNewValueES(char* szSource, char* szMsg, char* szTagName, double dNewValue)
    

    代码:

    #include "apdefap.h"
    /*---------------------------------------------------------------------------------------
    * 描述:
    *   执行电子签名,通过后向一个变量中写入新值,并将变量的新值和旧值记录到审计追踪,
    *   在审计追踪的reason列中写入"[szSource] szTagName: szMsg (szComments)"字符串。
    * 参数:
    *   szSource    :该字符串表示这条记录的来源或类型
    *   szMsg       :该字符串表示修改变量的操作说明
    *   szTagName   :变量名
    *   dNewValue   :新值
    * 返回值:
    *   -1:函数执行遇到错误
    *    1:电子签名通过
    *    2:用户取消了电子签名
    *    3:三次电子签名未通过
    *-----------------------------------------------------------------------------------------*/
    int DoubleTagNewValueES(char* szSource, char* szMsg, char* szTagName, double dNewValue)
    {
        char szComments[2014]="";
        double dOldValue;
        char szOldValue[512]="";
        char szNewValue[512]="";
        char szbuffer[512]="";
        int iRet = EsigDialog (szComments, FALSE, "");
        if ( iRet == 1 ) {
            dOldValue = GetTagDouble(szTagName);
            SetTagDouble(szTagName, dNewValue);
            sprintf(szOldValue,"%f",dOldValue);
            sprintf(szNewValue,"%f",dNewValue);
            sprintf(szbuffer,"%s: %s", szTagName, szMsg);
            CreateOpMsg(szSource, szbuffer, szOldValue, szNewValue, szComments);
        }
        return iRet;
    }
    

      

    向字符串类型的变量写入新值

    函数原型:

    int StrTagNewValueES(char* szSource, char* szMsg, char* szTagName, char* szNewValue)
    

    代码:

    #include "apdefap.h"
    /*-------------------------------------------------------------------------------------
    * 描述:
    *   执行电子签名,通过后向一个变量中写入新值,并将变量的新值和旧值记录到审计追踪,
    *   在审计追踪的reason列中写入"[szSource] szTagName: szMsg (szComments)"字符串。
    * 参数:
    *   szSource    :该字符串表示这条记录的来源或类型
    *   szMsg       :该字符串表示修改变量的操作说明
    *   szTagName   :变量名
    *   szNewValue  :新值
    * 返回值:
    *   -1:函数执行遇到错误
    *    1:电子签名通过
    *    2:用户取消了电子签名
    *    3:三次电子签名未通过
    *--------------------------------------------------------------------------------------*/
    int StrTagNewValueES(char* szSource, char* szMsg, char* szTagName, char* szNewValue)
    {
        char szComments[2014]="";
        char szOldValue[512]="";
        char szbuffer[512]="";
        int iRet = EsigDialog (szComments, FALSE ,"");
        if (iRet == 1 ) {
            sprintf(szOldValue,"%s", GetTagChar(szTagName) );
            SetTagChar(szTagName, szNewValue);
            sprintf(szbuffer,"%s: %s", szTagName, szMsg);
            CreateOpMsg(szSource, szbuffer, szOldValue, szNewValue, szComments);
        }
        return iRet;
    }
    

     

    查看Audit记录

    WinCC提供了AuditViewer控件查看Audit记录,该控件显示的示例如下。

     AuditViewer控件会显示Audit数据库中的所有列,不能通过配置隐藏某些不需要的列。下面提供一种参考方法,通过代码查询Audit数据显示到表格控件中。

    该方法是用VBS查询数据库,把查询到的数据显示到MSHFlexGrid控件,控件名为Grid。查询按钮中的代码见下方。

    Sub OnClick(Byval Item)                                                                                                                                            
    	'获取筛选时间
    	Dim timeFilter
    	Dim BeginTime, EndTime
    	BeginTime=DateValue(ScreenItems("BeginDate").value)+TimeValue(ScreenItems("Begintime").value)
    	EndTime=DateValue(ScreenItems("EndDate").value)+TimeValue(ScreenItems("Endtime").value)
    	If Datediff("s",BeginTime,EndTime) <= 0 Then
    		Msgbox TranslateText("时间选择不正确!", 2052)
    		Exit Sub
    	End If
    	timeFilter =  " Dateadd(hh,[TimeZoneOffset],[DateTime])>= '"&BeginTime& ".000' AND Dateadd(hh,[TimeZoneOffset],[DateTime])<= '"&EndTime&".000' "
    	timeFilter = Replace(timeFilter,"/","-")
    
    	Dim cnn, rs,cnnStr, SQL, i
        Set cnn = CreateObject("ADODB.Connection")
        Set rs = CreateObject("ADODB.Recordset")
        
        '查询Audit的数据库名称  
        Dim DatasourceNameRT, DatasourceNameRC, auditDataBase
        DatasourceNameRT = HMIRuntime.Tags("@NOP::@DatasourceNameRT").Read
        DatasourceNameRC = Left(DatasourceNameRT, Len(DatasourceNameRT)-1)
        cnnStr = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog="&DatasourceNameRC&";Data Source=" & HMIRuntime.Tags("@NOP::@ServerName").Read & "WINCC"
        cnn.ConnectionString = cnnStr
        cnn.CursorLocation = 3
        cnn.Open
    	SQL = "select DBName from [CC_Audit_SelectionsRT]"
    	rs.open SQL, cnn, 1, 3
    	If rs.EOF Then
    		Msgbox TranslateText("Audit数据库不存在!", 2052)
    		Exit Sub
    	Else
    		auditDataBase = rs("DBName")
    	End If
    	rs.close
    	cnn.Close
    	
    	'查询Audit数据库
       	cnnStr = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog="&auditDataBase&";Data Source=" & HMIRuntime.Tags("@NOP::@ServerName").Read & "WINCC"
        cnn.ConnectionString = cnnStr
        cnn.CursorLocation = 3
        cnn.Open
        
        Dim table,EOF_flag
        Dim GridControl
        Set GridControl = ScreenItems("Grid")
        table = "CL_TRAILDATA"
        'SQL = "select SWITCHOFFSET([DateTime],[TimeZoneOffset]*60) as DateTime,[ApplicationUser],[ComputerName],[TargetName],[OldValue],[NewValue],[Reason] from "&table&" where "&timeFilter
        SQL = "select Dateadd(hh,[TimeZoneOffset],[DateTime]) as DateTime,[ApplicationUser],[ComputerName],[TargetName],[OldValue],[NewValue],[Reason] from "&table&" where "&timeFilter & "ORDER BY DateTime DESC"
        'HMIRuntime.Trace SQL&vbNewline
        rs.open SQL, cnn, 1, 3
    	EOF_flag = rs.EOF
    	GridControl.Clear
        GridControl.Recordset = rs
       	For i=1 To rs.RecordCount
       		GridControl.TextMatrix(i,0) = i
       	Next 
       	rs.close
       	
       	If Not EOF_flag Then
    	    GridControl.ColWidth(1) = 3000
    	    GridControl.ColWidth(2) = 1500
    	    GridControl.ColWidth(3) = 2000
    	  	GridControl.ColWidth(4) = 5000
    	  	GridControl.ColWidth(5) = 4000
    	  	GridControl.ColWidth(6) = 4000
    	  	GridControl.ColWidth(7) = 8000
    	End If
    
      	cnn.Close
        
    End Sub
    

      

  • 相关阅读:
    [置顶] Java Web学习总结(25)——MyEclipse+Tomcat+MAVEN+SVN项目完整环境搭建
    [置顶] Java Web学习总结(25)——MyEclipse+Tomcat+MAVEN+SVN项目完整环境搭建
    [置顶] 青春路上,岁月如烟
    [置顶] 青春路上,岁月如烟
    [置顶] 青春路上,岁月如烟
    【立即报名】人脸情绪识别案例分享
    基于Docker快速搭建ELK
    【云速建站】表单应用
    【云速建站】页面产品维护简述
    Forrester:华为云容器是容器混合云最佳选择
  • 原文地址:https://www.cnblogs.com/yada/p/12031519.html
Copyright © 2011-2022 走看看