04 | 用 Jenkins 打通 CI自动构建镜像并推送到本地 Harbor

为 llm-serving 项目搭一条最小可用的 CI 流水线:Jenkins 从 GitHub 拉取代码,构建两个镜像(FastAPI 的 llm-serving、llama.cpp 推理的 llm-serving-llama),打上构建号 和 latest 两个标签,推送到本地 Harbor 的 library 项目。

备注:构建号BUILD_NUMBER 是 Jenkins 自动提供的环境变量,无须手动定义。

流程

复制代码
GitHub (lglgl/llm-serving, main)
   │  Pipeline script from SCM
   ▼
Jenkins Pipeline: llm-serving-ci
   │  docker build
   ▼
本地镜像 llm-serving / llm-serving-llama
   │  docker login (harbor-credentials) + docker push
   ▼
Harbor  harbor.local:30080/library/
   ├─ llm-serving:<构建号> / latest
   └─ llm-serving-llama:<构建号> / latest

过程概览

步骤 操作 关键内容
1 确认 Harbor 可访问 http://harbor.local:30080,默认用户 admin
2 在 Jenkins 配置 Harbor 凭据 Username with password,ID 为 harbor-credentials
3 确认 Jenkins 能执行 Docker 构建 流水线里能调用 docker 命令
4 创建 Jenkinsfile 构建、登录、推送两个镜像
5 配置 Jenkins 执行器 保证有可用的执行器来跑构建
6 在 Jenkins 创建任务 Pipeline script from SCM,Git 仓库 + */main + Jenkinsfile
7 构建并查看日志 Build Now,Console Output 显示 SUCCESS
8 验收 从 Harbor 拉取镜像,digest 与推送日志一致

验证结果

  • 第 6 次构建(Build Number 6)的最终结果为 SUCCESS,日志末尾输出 所有镜像已推送到 Harbor。
  • 两个镜像都已推送到 harbor.local:30080/library/:llm-serving-llama 的 6 与 latest 指向同一个 digest sha256:dea4451c...;llm-serving:latest 的 digest 是 sha256:71f0b338...。
  • 验收时 docker pull 两个 latest 镜像,得到的 digest 与 Jenkins 推送日志里的完全一致。

环境信息(来自记录)

  • 宿主机:MacBook,项目目录 devops-platform
  • 镜像仓库:Harbor,http://harbor.local:30080,项目 library
  • Jenkins 任务:llm-serving-ci(Pipeline 类型)
  • 代码仓库:https://github.com/lglgl/llm-serving.git,分支 */main,脚本路径 Jenkinsfile
  • 凭据 ID:harbor-credentials
  • 流水线结束时间:Mon Oct 12 01:17:25 CST 2026

一、确认 Harbor 可访问

先确认 Harbor 本身是可用的


二、在 Jenkins 配置 Harbor 凭据

2.1 添加凭据

进入 Jenkins:

text 复制代码
Manage Jenkins → Credentials → System → Global credentials → Add Credentials

填写:

字段 值
Kind Username with password
Username admin
Password <Harbor 密码>
ID harbor-credentials

要点:

  • ID 要和 Jenkinsfile 里引用的一致 (harbor-credentials),流水线通过这个 ID 取出用户名和密码,不会把密码写进脚本。
  • 本文把真实密码替换成了占位符 <Harbor 密码>,不要把密码写进博客或 Git 仓库。

2.2 在本机验证 Harbor 登录

在 Jenkins 之外,先用同样的账号在本机验证一次登录,确认账号密码和仓库地址都没问题:

bash 复制代码
(base) igwanglyang@MacbookProdahouzicn devops-platform % docker logout harbor.local:30080
docker login harbor.local:30080
Removing login credentials for harbor.local:30080
Username: admin
Password:
Login Succeeded
(base) igwanglyang@MacbookProdahouzicn devops-platform %

先 docker logout 再 docker login,可以排除"之前保存的旧登录状态"带来的干扰;输出 Login Succeeded 说明凭据有效。


三、确认 Jenkins 能执行 Docker 构建

流水线的核心动作是 docker build 和 docker push,所以要先确认 Jenkins 的执行环境里能调用 Docker。

补充:原始记录这一步只有标题。一个常用的检查办法是先建一个临时 Pipeline,在里面执行:

groovy 复制代码
pipeline {
  agent any
  stages {
    stage('Check Docker') {
      steps {
        sh 'docker version'
        sh 'docker info | head -n 20'
      }
    }
  }
}

如果输出里能看到 Client 和 Server 两部分版本信息,说明 Jenkins 能访问 Docker daemon。如果报 docker: command not found 或无法连接 daemon,需要为 Jenkins 的运行环境安装 Docker CLI 并让它能访问 Docker daemon(具体做法取决于 Jenkins 是怎么部署的)。同时要确认 Jenkins 的执行环境能解析 harbor.local。


四、创建 Jenkinsfile

在代码仓库根目录创建 Jenkinsfile(对应任务里的 Script Path)。

groovy 复制代码
pipeline {
    agent any

    options {
        // 禁止并发构建,避免同时覆盖镜像标签
        disableConcurrentBuilds()

        // 整个流水线最多运行 60 分钟
        timeout(time: 60, unit: 'MINUTES')
    }

    environment {
        // Harbor 镜像仓库地址,当前使用 HTTP
        HARBOR = 'harbor.local:30080'

        // Harbor 项目名称
        HARBOR_PROJECT = 'library'

        // FastAPI 服务镜像
        APP_IMAGE = 'llm-serving'

        // llama.cpp + Qwen 服务镜像
        LLAMA_IMAGE = 'llm-serving-llama'

        // Docker Desktop Kubernetes 的本机入口
        HARBOR_IP = '127.0.0.1'
    }

    stages {
        // ============================================================
        // 阶段 1:检出 GitHub 代码
        // ============================================================
        stage('Checkout') {
            steps {
                echo '========== 阶段 1:检出 GitHub 代码 =========='

                // 使用 Jenkins 任务中配置的 SCM
                checkout scm

                sh '''
                    set -eu

                    echo "当前工作目录:$(pwd)"
                    echo "当前 Git 提交:$(git rev-parse --short HEAD)"
                    echo "当前分支:$(git branch --show-current)"
                '''
            }
        }

        // ============================================================
        // 阶段 2:检查构建环境
        // ============================================================
        stage('Check Environment') {
            steps {
                echo '========== 阶段 2:检查构建环境 =========='

                sh '''
                    set -eu

                    # Jenkins 在 macOS 上运行时,补充 Homebrew 路径
                    export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

                    echo "操作系统:$(uname -s)"
                    echo "CPU 架构:$(uname -m)"
                    echo "当前用户:$(whoami)"
                    echo "PATH=$PATH"

                    # 检查 Docker CLI 是否存在
                    if ! command -v docker >/dev/null 2>&1; then
                        echo "ERROR:找不到 Docker CLI"
                        exit 1
                    fi

                    echo "Docker CLI 路径:$(command -v docker)"

                    # 同时检查 Docker Client 和 Docker Server
                    docker version

                    echo "Docker 环境检查通过"
                '''
            }
        }

        // ============================================================
        // 阶段 3:检查 Harbor
        // ============================================================
        stage('Check Harbor') {
            steps {
                echo '========== 阶段 3:检查 Harbor =========='

                sh '''
                    set -eu

                    HARBOR_URL="http://$HARBOR"

                    echo "Harbor 地址:$HARBOR_URL"
                    echo "连接 IP:$HARBOR_IP"

                    # 使用 --resolve 绕过系统 DNS 查询。
                    # HTTP Host 仍然是 harbor.local:30080,
                    # 因此 NGINX Ingress 可以匹配 Harbor 路由。

                    echo "检查 Harbor Portal..."

                    portal_status=$(curl --silent --show-error \
                        --connect-timeout 5 \
                        --max-time 10 \
                        --resolve "harbor.local:30080:$HARBOR_IP" \
                        -o /dev/null \
                        -w '%{http_code}' \
                        "$HARBOR_URL/")

                    if [ "$portal_status" != "200" ]; then
                        echo "ERROR:Harbor Portal 检查失败"
                        echo "HTTP 状态码:$portal_status"
                        echo "请检查 Ingress Host 和 Harbor Portal 后端"
                        exit 1
                    fi

                    echo "Harbor Portal 检查通过"

                    # 检查 Docker Registry API
                    echo "检查 Harbor Registry API..."

                    registry_status=$(curl --silent --show-error \
                        --connect-timeout 5 \
                        --max-time 10 \
                        --resolve "harbor.local:30080:$HARBOR_IP" \
                        -o /dev/null \
                        -w '%{http_code}' \
                        "$HARBOR_URL/v2/")

                    # 未登录时,401 通常表示 Registry 已响应认证挑战
                    case "$registry_status" in
                        200|401)
                            echo "Registry API 检查通过"
                            echo "HTTP 状态码:$registry_status"
                            ;;
                        *)
                            echo "ERROR:Registry API 检查失败"
                            echo "HTTP 状态码:$registry_status"
                            exit 1
                            ;;
                    esac

                    echo "Harbor 连通性检查通过"
                '''
            }
        }

        // ============================================================
        // 阶段 4:检查项目文件
        // ============================================================
        stage('Validate Files') {
            steps {
                echo '========== 阶段 4:检查项目文件 =========='

                sh '''
                    set -eu

                    # 检查 FastAPI 和 llama.cpp 镜像的构建文件
                    test -f Dockerfile
                    test -f requirements.txt
                    test -f llama/Dockerfile

                    echo "构建文件检查通过"

                    # 确保大型 GGUF 模型没有被提交到 Git
                    MODEL_FILE="llama/models/qwen2-0_5b-instruct-q4_k_m.gguf"

                    if git ls-files --error-unmatch "$MODEL_FILE" \
                        >/dev/null 2>&1; then
                        echo "ERROR:GGUF 模型文件不能提交到 Git"
                        exit 1
                    fi

                    echo "GGUF 模型未被 Git 跟踪"
                '''
            }
        }

        // ============================================================
        // 阶段 5:构建 FastAPI 镜像
        // ============================================================
        stage('Build llm-serving') {
            options {
                timeout(time: 10, unit: 'MINUTES')
            }

            steps {
                echo '========== 阶段 5:构建 llm-serving 镜像 =========='

                sh '''
                    set -eu
                    export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

                    IMAGE="$HARBOR/$HARBOR_PROJECT/$APP_IMAGE"

                    echo "目标镜像:$IMAGE:$BUILD_NUMBER"
                    echo "开始时间:$(date)"

                    # 构建 FastAPI 服务镜像
                    docker build --progress=plain --pull \
                        -t "$IMAGE:$BUILD_NUMBER" \
                        -t "$IMAGE:latest" \
                        -f Dockerfile .

                    echo "llm-serving 镜像构建成功"
                    echo "结束时间:$(date)"
                '''
            }
        }

        // ============================================================
        // 阶段 6:构建 llama.cpp + Qwen 镜像
        // ============================================================
        stage('Build llama + Qwen') {
            options {
                // 模型下载可能耗时较长
                timeout(time: 20, unit: 'MINUTES')
            }

            steps {
                echo '========== 阶段 6:构建 llama + Qwen 镜像 =========='

                sh '''
                    set -eu
                    export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

                    IMAGE="$HARBOR/$HARBOR_PROJECT/$LLAMA_IMAGE"

                    echo "目标镜像:$IMAGE:$BUILD_NUMBER"
                    echo "开始时间:$(date)"

                    # 模型文件不提交到 Git,由 llama/Dockerfile 在构建时下载
                    docker build --progress=plain --pull \
                        -t "$IMAGE:$BUILD_NUMBER" \
                        -t "$IMAGE:latest" \
                        -f llama/Dockerfile llama

                    echo "llama + Qwen 镜像构建成功"
                    echo "结束时间:$(date)"
                '''
            }
        }

        // ============================================================
        // 阶段 7:验证本地镜像
        // ============================================================
        stage('Verify Images') {
            steps {
                echo '========== 阶段 7:验证本地镜像 =========='

                sh '''
                    set -eu
                    export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

                    # 检查两个镜像的版本标签是否存在
                    docker image inspect \
                        "$HARBOR/$HARBOR_PROJECT/$APP_IMAGE:$BUILD_NUMBER" \
                        >/dev/null

                    docker image inspect \
                        "$HARBOR/$HARBOR_PROJECT/$LLAMA_IMAGE:$BUILD_NUMBER" \
                        >/dev/null

                    echo "两个镜像均已成功生成"
                '''
            }
        }

        // ============================================================
        // 阶段 8:推送镜像到 Harbor
        // ============================================================
        stage('Push to Harbor') {
            options {
                timeout(time: 10, unit: 'MINUTES')
            }

            steps {
                echo '========== 阶段 8:推送镜像到 Harbor =========='

                // 从 Jenkins Credentials 获取 Harbor 账号和密码
                withCredentials([
                    usernamePassword(
                        credentialsId: 'harbor-credentials',
                        usernameVariable: 'HARBOR_USER',
                        passwordVariable: 'HARBOR_PASSWORD'
                    )
                ]) {
                    sh '''
                        set -eu
                        set +x

                        export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

                        # 推送结束后尝试清理 Docker 登录状态
                        trap 'docker logout "$HARBOR" >/dev/null 2>&1 || true' EXIT

                        echo "登录 Harbor:$HARBOR"

                        # 使用标准输入传递密码,避免将密码写入命令参数
                        printf '%s' "$HARBOR_PASSWORD" |
                            docker login "$HARBOR" \
                                -u "$HARBOR_USER" \
                                --password-stdin

                        # 推送 FastAPI 镜像的版本标签
                        echo "推送 $APP_IMAGE:$BUILD_NUMBER"

                        docker push \
                            "$HARBOR/$HARBOR_PROJECT/$APP_IMAGE:$BUILD_NUMBER"

                        # 推送 FastAPI 镜像的 latest 标签
                        echo "推送 $APP_IMAGE:latest"

                        docker push \
                            "$HARBOR/$HARBOR_PROJECT/$APP_IMAGE:latest"

                        # 推送 llama.cpp + Qwen 镜像的版本标签
                        echo "推送 $LLAMA_IMAGE:$BUILD_NUMBER"

                        docker push \
                            "$HARBOR/$HARBOR_PROJECT/$LLAMA_IMAGE:$BUILD_NUMBER"

                        # 推送 llama.cpp + Qwen 镜像的 latest 标签
                        echo "推送 $LLAMA_IMAGE:latest"

                        docker push \
                            "$HARBOR/$HARBOR_PROJECT/$LLAMA_IMAGE:latest"

                        echo "所有镜像已推送到 Harbor"
                    '''
                }
            }
        }
    }

    // ================================================================
    // 流水线结束处理
    // ================================================================
    post {
        success {
            echo '''
            ========================================
            CI 成功
            llm-serving 和 llama + Qwen 镜像已推送。
            请在 Harbor 中检查镜像版本。
            ========================================
            '''
        }

        failure {
            echo '''
            ========================================
            CI 失败
            请根据最后一个失败阶段定位问题。
            优先检查 Docker、网络、模型下载和 Harbor 认证。
            ========================================
            '''
        }

        always {
            echo "Pipeline 结束时间:${new Date()}"
            echo "Jenkins Build Number:${env.BUILD_NUMBER}"
            echo "最终构建结果:${currentBuild.currentResult}"
        }
    }
}

设计要点:

  • 双标签 :每个镜像推送两次,一个是构建号(如 6,可追溯到具体某次构建),一个是 latest(方便默认拉取)。
  • 凭据不落盘 :用 withCredentials 取出用户名和密码,再通过 --password-stdin 登录,密码不会出现在命令行参数里。
  • timeout 兜底:避免某次推送卡住导致流水线一直占着执行器。
  • post { always { ... } }:无论成功失败都输出结束时间、构建号、最终结果。
  • 注意 :构建 llama/Dockerfile 时需要 llama/models/qwen2-0_5b-instruct-q4_k_m.gguf 在工作区里存在(约 390MB),要保证它能随代码被 Jenkins 取到(例如提交进仓库、使用 Git LFS,或在流水线里提前下载)。

五、配置 Jenkins 执行器

补充 :原始记录这一步只有一个标题"Jenkins 配置执行器",没有内容。执行器(executor)决定了 Jenkins 同时能跑几个构建,流水线 agent any 需要至少有一个可用的执行器才能开始。

一般的配置位置是:

text 复制代码
Manage Jenkins → Nodes → Built-In Node → Configure → Number of executors

对这样的单人本地环境,把执行器数量设为 1 或 2 即可;设置完成后保存。请把你实际设置的数量和位置补充到这里。


六、在 Jenkins 创建任务

  1. 进入 Jenkins 首页,选择 New Item。

  2. 名称填写 llm-serving-ci,类型选择 Pipeline。

  3. 在 Pipeline 配置中选择 Pipeline script from SCM。

  4. SCM 选择 Git,填写 GitHub 仓库地址和对应凭据:

    text 复制代码
    https://github.com/lglgl/llm-serving.git
  5. 分支填写 */main,Script Path 填写 Jenkinsfile。

  6. 保存后点击 Build Now ,查看 Console Output。

要点:

  • 选 Pipeline script from SCM 后,流水线定义随代码一起版本管理,改 Jenkinsfile 就是改 CI,不用到 Jenkins 页面里改脚本。
  • 分支 */main 要和仓库实际的默认分支一致;Script Path 必须与仓库中 Jenkinsfile 的相对路径一致。
  • 仓库如果是私有的,需要为 Jenkins 另外配置能拉取 GitHub 的凭据(与 Harbor 凭据是两回事)。

七、构建并查看 Console Output

点击 Build Now 后,从 Console Output 里可以看到推送阶段的日志(此处为第 6 次构建的末段):

text 复制代码
84dd5a4db43e: Layer already exists

373ce1c1778b: Layer already exists
11d89b6e886f: Layer already exists
258ba1f62b51: Layer already exists

latest: digest: sha256:71f0b338f88c7fdcc97f947d79560bf224d4f642bcdfeb46bbb93f32fae6d7f8 size: 1992
推送 llm-serving-llama:6

The push refers to repository [harbor.local:30080/library/llm-serving-llama]

1d76195dc42f: Preparing
5f70bf18a086: Preparing
bbd7bf824dec: Preparing
6038fa08e79b: Preparing
14887e27a87e: Preparing
1e9178dc9005: Preparing
1e9178dc9005: Waiting

bbd7bf824dec: Pushed
6038fa08e79b: Pushed
5f70bf18a086: Pushed

1d76195dc42f: Pushed

14887e27a87e: Pushed

1e9178dc9005: Pushed

6: digest: sha256:dea4451c2031a02617c5cc45c78fda418436b510cd7d155abf09b0c2435fa7e0 size: 1582
推送 llm-serving-llama:latest

The push refers to repository [harbor.local:30080/library/llm-serving-llama]

1d76195dc42f: Preparing
5f70bf18a086: Preparing
bbd7bf824dec: Preparing
6038fa08e79b: Preparing
14887e27a87e: Preparing
1e9178dc9005: Preparing
1e9178dc9005: Waiting

6038fa08e79b: Layer already exists
14887e27a87e: Layer already exists
5f70bf18a086: Layer already exists
bbd7bf824dec: Layer already exists
1d76195dc42f: Layer already exists

1e9178dc9005: Layer already exists

latest: digest: sha256:dea4451c2031a02617c5cc45c78fda418436b510cd7d155abf09b0c2435fa7e0 size: 1582
所有镜像已推送到 Harbor

[Pipeline] }
[Pipeline] // withCredentials
[Pipeline] }
[Pipeline] // timeout
[Pipeline] }
[Pipeline] // stage

[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] echo
Pipeline 结束时间:Mon Oct 12 01:17:25 CST 2026
[Pipeline] echo
Jenkins Build Number:6
[Pipeline] echo
最终构建结果:SUCCESS

日志解读:

  • 第一段(llm-serving:latest) :四层全部是 Layer already exists,说明同样的层已经随 llm-serving:6 推送过了;latest 只是给同一个 digest 再打了一个标签,最后一行 digest 为 sha256:71f0b338...。
  • 推送 llm-serving-llama:6 :先推 6 标签,这次有真实的层上传(Pushed),得到 digest sha256:dea4451c...、size 1582。
  • 推送 llm-serving-llama:latest :这次所有层都是 Layer already exists,digest 仍是 sha256:dea4451c...。两个标签指向同一份内容,所以只多了一次元数据写入,几乎不花时间。
  • 结尾 :withCredentials、timeout、stage 依次正常结束,Post Actions 输出结束时间 Mon Oct 12 01:17:25 CST 2026、构建号 6、最终结果 SUCCESS。

八、验收

构建成功后,把两个镜像从 Harbor 拉取一遍:

bash 复制代码
(base) igwanglyang@MacbookProdahouzicn devops-platform % docker pull harbor.local:30080/library/llm-serving:latest
docker pull harbor.local:30080/library/llm-serving-llama:latest
latest: Pulling from library/llm-serving
Digest: sha256:71f0b338f88c7fdcc97f947d79560bf224d4f642bcdfeb46bbb93f32fae6d7f8
Status: Image is up to date for harbor.local:30080/library/llm-serving:latest
harbor.local:30080/library/llm-serving:latest
latest: Pulling from library/llm-serving-llama
Digest: sha256:dea4451c2031a02617c5cc45c78fda418436b510cd7d155abf09b0c2435fa7e0
Status: Image is up to date for harbor.local:30080/library/llm-serving-llama:latest
harbor.local:30080/library/llm-serving-llama:latest

核对点:

镜像 拉取得到的 digest 与 Jenkins 推送日志
llm-serving:latest sha256:71f0b338f88c...d7f8 一致
llm-serving-llama:latest sha256:dea4451c2031...fa7e0 一致(同 6 标签)
  • digest 一致,说明 Harbor 里的 latest 就是这次流水线刚推送的内容。
  • Image is up to date 表示本机已经有同一 digest 的镜像,无需再下载。

补充 :也可以登录 Harbor 网页 http://harbor.local:30080,进入 library 项目,检查 llm-serving 与 llm-serving-llama 两个仓库里是否有 6 和 latest 两个标签,并确认它们的 digest 与上面一致。


后续计划

当前流水线只做"构建 + 推送"。后续再集成安全扫描:

常见做法有两种------一是在 Harbor 里对 library 项目开启"推送时自动扫描"(Harbor 通常自带 Trivy 扫描器);二是在 Jenkinsfile 里增加一个扫描阶段,在推送前后用 Trivy 等工具检查镜像漏洞,并设定严重级别阈值,不达标就让流水线失败。


总结

这条 CI 流水线的完整路径:

text 复制代码
1. 确认 Harbor 可访问:http://harbor.local:30080(admin)
2. Jenkins 添加凭据:Username with password,ID = harbor-credentials
3. 确认 Jenkins 能执行 docker 命令
4. 仓库根目录创建 Jenkinsfile:构建 → 登录 → 推送(构建号 + latest)
5. 配置 Jenkins 执行器
6. 新建 Pipeline 任务 llm-serving-ci:Pipeline script from SCM
   仓库 https://github.com/lglgl/llm-serving.git,分支 */main,Script Path = Jenkinsfile
7. Build Now → Console Output 显示 SUCCESS
8. docker pull harbor.local:30080/library/<镜像>:latest,核对 digest

经验点:

  1. 先在 Jenkins 之外验证 Harbor 登录 :docker logout + docker login 排除账号、地址、HTTP 仓库配置的问题,再去配 Jenkins 凭据。
  2. 凭据用 ID 引用,不写进脚本 :withCredentials + --password-stdin,密码既不进 Jenkinsfile,也不出现在命令行里。
  3. 双标签策略 :构建号用来追溯,latest 用来默认拉取;两个标签指向同一 digest,第二次推送几乎零成本。
  4. 流水线即代码 :Pipeline script from SCM 让 Jenkinsfile 跟随代码版本管理。
  5. 用 digest 验收 :推送日志里的 digest 与 docker pull 的 digest 一致,才算真正确认镜像进了 Harbor。
  6. 安全扫描留到下一步:先跑通"构建 + 推送"闭环,再叠加漏洞扫描。
相关推荐
吴声子夜歌1 小时前
Docker入门与实战——实战案例(编程开发)(一)
运维·docker·容器
海宇AI1 小时前
零信任架构实战:基于海宇运营商近3个月平均账单构建自动化免押租赁授信网关
运维·人工智能·架构·自动化
此生决int1 小时前
Linux系列(06)——进程概念(上)
linux·运维·服务器
海宇服务1 小时前
零信任架构实战:基于海宇股权穿透构建自动化招投标供应商关联排查网关
运维·人工智能·架构·自动化
wdfk_prog2 小时前
LWIP教程 13:从 `dhcp_start()` 到 `DHCP_STATE_BOUND`——DHCPv4、ACD、Timer 与真实抓包
运维·网络协议
星云低代码开发平台2 小时前
AI 连接器运行健康度:指标、异常恢复和联调清单
大数据·运维·网络·数据库·人工智能·mcp
Sayai2 小时前
Hadoop 集群安全加固实战:YARN 未授权访问漏洞修复,firewalld 白名单 + Nginx 反向代理 Basic Auth 全流程
运维·hadoop·nginx·安全·网络安全
云飞云共享云桌面3 小时前
智能装备数字化转型:云飞云云桌面助力苏州工厂共享 SolidWorks 算力
运维·服务器·3d·自动化·制造
Zkaisen3 小时前
Jenkins 任意文件读取漏洞(CVE-2024-23897)安全防御指南
jenkins