016-现代桌面应用型IT自动化运维系统的设计与实现
本文介绍了现代桌面应用型IT自动化运维系统的设计与实现。该系统采用C/S架构,包含服务器资产管理、监控告警、远程控制、配置管理、自动化任务等核心功能模块。系统架构设计包括客户端(图形化界面)、服务器端(业务逻辑处理)和代理程序(部署在被管理服务器上)三部分。数据库采用关系型数据库存储配置和资产数据,时序数据库存储监控数据。系统支持跨平台部署,提供高可用性设计和严格的安全控制,包括认证授权、数据加密
现代桌面应用型IT自动化运维系统的设计与实现
随着企业IT基础设施规模不断扩大和复杂度不断提高,传统的手动运维方式已无法满足现代企业的需求。构建一个高效、稳定、易用的自动化运维平台成为企业IT部门的迫切需求。本章将详细介绍如何设计和实现一个基于C/S架构的桌面版自动化运维平台,该平台能够提供丰富的功能,帮助IT管理员高效管理和维护企业IT基础设施。
16.1 平台功能介绍
桌面版C/S自动化运维平台是一款面向中小型企业IT部门的运维管理工具,通过客户端和服务器的架构,为IT管理员提供全面的基础设施管理和自动化运维能力。本节将详细介绍平台的核心功能模块和主要特点。
核心功能模块
服务器资产管理
服务器资产管理模块是运维平台的基础功能,负责管理企业内所有服务器的基本信息:
- 基本信息管理:记录服务器的品牌、型号、序列号、购买日期、保修期等基本信息
- 配置信息管理:记录服务器的CPU、内存、硬盘、网卡等硬件配置信息
- 系统信息管理:记录服务器的操作系统类型、版本、安装日期等信息
- 网络信息管理:记录服务器的IP地址、MAC地址、网络区域等信息
- 责任人管理:记录服务器的管理员、使用部门等信息
- 分类管理:支持按照应用、环境、部门等多种维度对服务器进行分类
监控告警系统
监控告警系统负责实时监控服务器的运行状态,并在发现异常时及时通知管理员:
- 性能监控:监控CPU使用率、内存使用率、磁盘使用率、网络流量等性能指标
- 服务监控:监控关键服务的运行状态,如数据库服务、Web服务、应用服务等
- 日志监控:收集和分析系统日志、应用日志,检测异常情况
- 阈值设置:支持为各类监控项设置告警阈值
- 告警通知:支持邮件、短信、企业微信等多种告警通知方式
- 告警级别:支持设置不同的告警级别,如信息、警告、错误、严重等
- 告警处理:支持记录告警处理过程和结果
远程控制管理
远程控制管理模块提供对服务器的远程访问和控制能力:
- 远程终端:提供基于SSH、Telnet的命令行终端访问
- 远程桌面:提供Windows远程桌面和VNC远程访问
- 文件传输:支持SFTP、FTP文件上传下载
- 批量操作:支持对多台服务器同时执行命令或脚本
- 会话管理:记录远程操作会话,支持会话回放
- 权限控制:基于角色的远程访问权限控制
配置管理
配置管理模块负责管理服务器的配置信息,确保配置的一致性和合规性:
- 配置采集:自动采集服务器的配置信息
- 配置比对:比较不同服务器之间的配置差异
- 配置模板:创建标准配置模板
- 配置部署:快速部署标准配置
- 配置备份:定期备份重要配置文件
- 配置审计:检查配置是否符合安全标准和最佳实践
自动化任务
自动化任务模块提供强大的任务编排和执行能力:
- 任务编排:通过可视化界面创建复杂的自动化任务流程
- 计划任务:设置定时执行的任务
- 触发任务:基于特定事件触发执行的任务
- 任务模板:预定义常用任务模板
- 执行记录:详细记录任务执行过程和结果
- 任务通知:任务执行完成后发送通知
软件管理
软件管理模块负责管理服务器上的软件:
- 软件清单:自动采集服务器上安装的软件清单
- 软件分发:集中分发软件包到多台服务器
- 软件更新:管理软件更新和升级
- 补丁管理:管理系统补丁的分发和安装
- 许可证管理:管理软件许可证信息
报表分析
报表分析模块提供丰富的数据分析和可视化功能:
- 资产报表:服务器资产统计报表
- 性能报表:服务器性能趋势分析
- 告警报表:告警统计和分析
- 任务报表:自动化任务执行统计
- 自定义报表:支持用户自定义报表
- 导出功能:支持将报表导出为PDF、Excel等格式
平台特点
桌面版C/S自动化运维平台具有以下特点:
- 易用性:通过图形化界面,降低使用门槛,使不具备编程能力的IT管理员也能快速上手
- 轻量级:客户端资源占用小,安装简便,适合中小型企业使用
- 安全性:支持多种身份认证方式,严格的权限控制,加密数据传输
- 扩展性:模块化设计,支持通过插件扩展功能
- 离线能力:支持离线工作,网络恢复后自动同步数据
- 跨平台:客户端支持Windows、macOS和Linux操作系统
- 定制化:支持按照企业需求进行界面和功能定制
16.2 系统构架设计
为了实现前述功能,我们需要设计一个灵活、可靠、安全的系统架构。本节将详细介绍桌面版C/S自动化运维平台的系统架构设计。
总体架构
系统采用典型的C/S(客户端/服务器)架构,主要包括以下部分:
-
客户端(Client):
- 表现层:用户界面,负责与用户交互
- 业务逻辑层:实现客户端的业务逻辑
- 数据访问层:处理与服务器的数据交换
- 本地存储:支持离线工作的本地数据库
- 插件系统:支持功能扩展的插件框架
-
服务器(Server):
- API网关:统一的API访问入口,处理认证授权
- 业务逻辑层:实现核心业务功能
- 数据访问层:处理数据库操作
- 任务调度系统:管理后台任务的执行
- 通知服务:处理各类通知和告警
- 文件存储服务:管理系统文件存储
-
代理(Agent):
- 部署在被管理服务器上的轻量级代理程序
- 负责采集数据、执行命令、监控状态
- 支持与服务器的加密通信
-
数据库:
- 存储系统配置数据
- 存储监控数据
- 存储资产信息
- 存储用户和权限数据
- 存储操作日志
-
外部集成:
- 邮件服务器:发送邮件通知
- 短信网关:发送短信通知
- 企业微信/钉钉:发送即时消息
- LDAP/AD:集成企业目录服务
- 其他第三方系统
技术架构
客户端技术栈
- 开发语言:C#/.NET
- UI框架:WPF (Windows Presentation Foundation)
- 本地数据库:SQLite
- 网络通信:gRPC, WebSocket, REST API
- 数据序列化:JSON, Protocol Buffers
- 安全组件:TLS/SSL, AES加密
- 日志框架:NLog
- 依赖注入:Microsoft.Extensions.DependencyInjection
- 插件框架:MEF (Managed Extensibility Framework)
服务器技术栈
- 开发语言:C#/.NET Core
- Web框架:ASP.NET Core
- 数据库访问:Entity Framework Core
- API文档:Swagger/OpenAPI
- 认证授权:JWT (JSON Web Token), OAuth 2.0
- 消息队列:RabbitMQ
- 缓存:Redis
- 监控:Prometheus + Grafana
- 容器化:Docker
代理技术栈
- 开发语言:Go (适用于各种操作系统)
- 配置管理:YAML
- 数据收集:自定义插件 + 开源工具集成
- 安全通信:HTTPS, TLS/SSL
- 本地缓存:LevelDB
数据流设计
系统的主要数据流如下:
-
资产数据流:
- Agent采集服务器信息 → 上报给Server → 存储到数据库 → Client查询显示
- Client手动录入服务器信息 → 提交给Server → 存储到数据库
-
监控数据流:
- Agent采集监控指标 → 上报给Server → 存储到数据库 → 分析处理 → 触发告警 → 通知服务 → 发送告警
- Client查询监控数据 → Server处理请求 → 返回数据 → Client展示
-
远程控制流:
- Client发起远程连接请求 → Server处理认证授权 → 建立与目标服务器的连接 → 双向数据传输
- Server记录操作日志 → 存储到数据库
-
自动化任务流:
- Client创建任务 → 提交给Server → 存储到数据库
- Server调度任务 → 分配给Agent执行 → Agent执行任务 → 返回结果 → Server处理结果 → 存储到数据库
- Client查询任务结果 → Server返回数据 → Client展示
系统安全设计
考虑到运维平台的敏感性,系统安全设计至关重要:
-
认证与授权:
- 支持多种认证方式:用户名密码、证书、LDAP/AD、双因素认证
- 基于角色的访问控制(RBAC)
- 最小权限原则
- 会话管理和超时控制
-
数据安全:
- 传输数据加密:TLS/SSL
- 敏感数据存储加密:密码、密钥等
- 数据完整性校验
- 定期数据备份
-
通信安全:
- 客户端与服务器间的通信加密
- 服务器与代理间的通信加密
- 证书验证和信任机制
- 防重放攻击
-
操作审计:
- 详细记录所有关键操作
- 不可篡改的审计日志
- 支持审计日志查询和分析
- 异常操作检测和告警
-
漏洞防护:
- 定期安全更新
- 代码安全审计
- 第三方组件漏洞扫描
- 安全开发生命周期
高可用设计
为保证系统的稳定运行,采用以下高可用设计:
-
服务器高可用:
- 主备架构或集群部署
- 负载均衡
- 故障自动转移
-
数据库高可用:
- 主从复制
- 自动备份
- 故障恢复机制
-
客户端容错:
- 本地缓存
- 离线工作模式
- 自动重连机制
-
代理高可用:
- 健康检查
- 自动恢复
- 数据缓冲
16.3 数据库结构设计
数据库是系统的核心组成部分,良好的数据库设计对系统的性能和可扩展性至关重要。本节将详细介绍系统的数据库结构设计。
16.3.1 数据库分析
在设计数据库之前,我们需要分析系统的数据需求和访问模式。
数据类型分析
系统涉及的主要数据类型如下:
-
配置数据:
- 系统配置参数
- 用户偏好设置
- 功能模块配置
- 数据量小,变化频率低
-
用户和权限数据:
- 用户信息
- 角色定义
- 权限分配
- 数据量小,变化频率低
-
资产数据:
- 服务器基本信息
- 硬件配置信息
- 系统信息
- 网络配置
- 数据量中等,变化频率低
-
监控数据:
- 性能监控指标
- 服务状态数据
- 告警记录
- 数据量大,变化频率高
-
操作日志:
- 用户操作记录
- 系统事件日志
- 审计日志
- 数据量大,只增不改
-
任务数据:
- 任务定义
- 任务执行记录
- 任务结果
- 数据量中等,变化频率中等
数据访问模式分析
不同类型的数据有不同的访问模式:
-
配置数据:
- 读多写少
- 全表查询为主
- 高一致性要求
-
用户和权限数据:
- 读多写少
- 单条查询为主
- 高一致性要求
-
资产数据:
- 读多写少
- 条件查询为主
- 中等一致性要求
-
监控数据:
- 写入频繁
- 时序查询为主
- 低一致性要求
-
操作日志:
- 只写入,几乎不修改
- 条件查询为主
- 低一致性要求
-
任务数据:
- 读写均衡
- 状态更新频繁
- 中等一致性要求
数据量估算
假设一个中型企业环境:
- 服务器数量:500台
- 用户数量:50人
- 每台服务器监控指标:50个
- 监控采集频率:每分钟一次
- 每天执行任务数:100个
数据量估算:
- 资产数据:约500KB(不含历史记录)
- 每日监控数据:500台 × 50指标 × 60分钟 × 24小时 × 100字节 ≈ 3.6GB
- 每日操作日志:约50MB
- 每日任务数据:约10MB
根据上述分析,我们需要采用合适的数据库来存储不同类型的数据:
- 关系型数据库(如SQL Server, MySQL):存储配置数据、用户权限数据、资产数据和任务数据
- 时序数据库(如InfluxDB, TimescaleDB):存储监控数据
- 文档数据库(可选,如MongoDB):存储非结构化或半结构化数据
16.3.2 数据字典
基于上述分析,我们定义系统的主要数据实体和属性。
用户和权限管理
-
Users(用户表)
UserID(PK): int, 用户IDUsername: varchar(50), 用户名PasswordHash: varchar(256), 密码哈希Salt: varchar(50), 密码盐值Email: varchar(100), 电子邮箱FullName: varchar(100), 用户全名PhoneNumber: varchar(20), 电话号码IsActive: bit, 是否激活LastLoginTime: datetime, 最后登录时间CreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间IsAdmin: bit, 是否是管理员AvatarPath: varchar(256), 头像路径
-
Roles(角色表)
RoleID(PK): int, 角色IDRoleName: varchar(50), 角色名称Description: varchar(200), 角色描述CreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
Permissions(权限表)
PermissionID(PK): int, 权限IDPermissionName: varchar(50), 权限名称Description: varchar(200), 权限描述PermissionCode: varchar(50), 权限代码ModuleCode: varchar(50), 模块代码CreatedTime: datetime, 创建时间
-
UserRoles(用户角色关联表)
UserRoleID(PK): int, 用户角色IDUserID(FK): int, 用户IDRoleID(FK): int, 角色IDAssignedTime: datetime, 分配时间
-
RolePermissions(角色权限关联表)
RolePermissionID(PK): int, 角色权限IDRoleID(FK): int, 角色IDPermissionID(FK): int, 权限IDAssignedTime: datetime, 分配时间
资产管理
-
Servers(服务器表)
ServerID(PK): int, 服务器IDHostname: varchar(100), 主机名IPAddress: varchar(39), IP地址MACAddress: varchar(17), MAC地址ServerType: varchar(50), 服务器类型OSType: varchar(50), 操作系统类型OSVersion: varchar(50), 操作系统版本CPUModel: varchar(100), CPU型号CPUCores: int, CPU核心数MemorySize: int, 内存大小(MB)DiskSize: int, 磁盘大小(GB)Department: varchar(100), 所属部门Location: varchar(100), 位置ResponsiblePerson: varchar(100), 负责人PurchaseDate: date, 购买日期WarrantyEndDate: date, 保修结束日期Status: varchar(20), 状态Description: text, 描述LastCheckTime: datetime, 最后检查时间CreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间AgentStatus: varchar(20), 代理状态AgentVersion: varchar(20), 代理版本
-
ServerGroups(服务器组表)
GroupID(PK): int, 组IDGroupName: varchar(100), 组名称Description: text, 描述ParentGroupID(FK): int, 父组IDCreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
ServerGroupMemberships(服务器组成员表)
MembershipID(PK): int, 成员IDServerID(FK): int, 服务器IDGroupID(FK): int, 组IDAddedTime: datetime, 添加时间
-
ServerNetworkInterfaces(服务器网络接口表)
InterfaceID(PK): int, 接口IDServerID(FK): int, 服务器IDInterfaceName: varchar(50), 接口名称IPAddress: varchar(39), IP地址MACAddress: varchar(17), MAC地址SubnetMask: varchar(39), 子网掩码Gateway: varchar(39), 网关DNSServers: varchar(200), DNS服务器IsActive: bit, 是否激活UpdatedTime: datetime, 更新时间
-
ServerDisks(服务器磁盘表)
DiskID(PK): int, 磁盘IDServerID(FK): int, 服务器IDDiskName: varchar(50), 磁盘名称DiskType: varchar(20), 磁盘类型TotalSize: int, 总大小(GB)FreeSize: int, 可用大小(GB)FileSystem: varchar(20), 文件系统MountPoint: varchar(200), 挂载点UpdatedTime: datetime, 更新时间
监控和告警
-
MonitorItems(监控项目表)
ItemID(PK): int, 项目IDItemName: varchar(100), 项目名称ItemType: varchar(50), 项目类型MetricName: varchar(50), 指标名称Description: text, 描述CollectionInterval: int, 采集间隔(秒)IsActive: bit, 是否激活CreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
MonitorThresholds(监控阈值表)
ThresholdID(PK): int, 阈值IDItemID(FK): int, 监控项目IDServerID(FK): int, 服务器ID (可为NULL表示全局阈值)GroupID(FK): int, 组ID (可为NULL)WarningThreshold: varchar(50), 警告阈值CriticalThreshold: varchar(50), 严重阈值ComparisonOperator: varchar(10), 比较操作符CreatedBy(FK): int, 创建人IDCreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
Alerts(告警表)
AlertID(PK): int, 告警IDServerID(FK): int, 服务器IDItemID(FK): int, 监控项目IDAlertLevel: varchar(20), 告警级别AlertMessage: text, 告警消息CurrentValue: varchar(50), 当前值ThresholdValue: varchar(50), 阈值FirstOccurTime: datetime, 首次发生时间LastOccurTime: datetime, 最后发生时间OccurrenceCount: int, 发生次数Status: varchar(20), 状态ProcessedBy(FK): int, 处理人IDProcessedTime: datetime, 处理时间ProcessingComments: text, 处理说明
-
NotificationRules(通知规则表)
RuleID(PK): int, 规则IDRuleName: varchar(100), 规则名称AlertLevel: varchar(20), 告警级别ItemID(FK): int, 监控项目ID (可为NULL)ServerID(FK): int, 服务器ID (可为NULL)GroupID(FK): int, 组ID (可为NULL)NotificationType: varchar(20), 通知类型NotificationTarget: varchar(200), 通知目标IsActive: bit, 是否激活CreatedBy(FK): int, 创建人IDCreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
任务管理
-
Tasks(任务表)
TaskID(PK): int, 任务IDTaskName: varchar(100), 任务名称TaskType: varchar(50), 任务类型ScriptContent: text, 脚本内容Parameters: text, 参数Description: text, 描述Timeout: int, 超时时间(秒)CreatedBy(FK): int, 创建人IDCreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
ScheduledTasks(计划任务表)
ScheduleID(PK): int, 计划IDTaskID(FK): int, 任务IDScheduleName: varchar(100), 计划名称ScheduleType: varchar(20), 计划类型CronExpression: varchar(100), Cron表达式StartTime: datetime, 开始时间EndTime: datetime, 结束时间IsActive: bit, 是否激活LastRunTime: datetime, 最后运行时间NextRunTime: datetime, 下次运行时间CreatedBy(FK): int, 创建人IDCreatedTime: datetime, 创建时间UpdatedTime: datetime, 更新时间
-
TaskTargets(任务目标表)
TargetID(PK): int, 目标IDTaskID(FK): int, 任务IDServerID(FK): int, 服务器ID (与GroupID二选一)GroupID(FK): int, 组ID (与ServerID二选一)AddedTime: datetime, 添加时间
-
TaskExecutions(任务执行表)
ExecutionID(PK): int, 执行IDTaskID(FK): int, 任务IDScheduleID(FK): int, 计划ID (可为NULL表示手动执行)ExecutionTime: datetime, 执行时间CompletionTime: datetime, 完成时间Status: varchar(20), 状态TriggerType: varchar(20), 触发类型TriggeredBy(FK): int, 触发人IDParentExecutionID(FK): int, 父执行ID (可为NULL)
-
TaskExecutionDetails(任务执行详情表)
DetailID(PK): int, 详情IDExecutionID(FK): int, 执行IDServerID(FK): int, 服务器IDStartTime: datetime, 开始时间EndTime: datetime, 结束时间Status: varchar(20), 状态Output: text, 输出内容ErrorMessage: text, 错误消息ReturnCode: int, 返回代码
系统配置和日志
-
SystemSettings(系统设置表)
SettingID(PK): int, 设置IDSettingKey: varchar(100), 设置键SettingValue: text, 设置值SettingGroup: varchar(50), 设置组Description: varchar(200), 描述UpdatedBy(FK): int, 更新人IDUpdatedTime: datetime, 更新时间
-
AuditLogs(审计日志表)
LogID(PK): int, 日志IDUserID(FK): int, 用户IDIPAddress: varchar(39), IP地址ModuleCode: varchar(50), 模块代码ActionType: varchar(50), 操作类型ActionTarget: varchar(200), 操作目标ActionContent: text, 操作内容ActionTime: datetime, 操作时间Status: varchar(20), 状态ErrorMessage: text, 错误消息
-
SystemLogs(系统日志表)
LogID(PK): int, 日志IDLogLevel: varchar(20), 日志级别Logger: varchar(100), 日志记录器Message: text, 消息Exception: text, 异常信息ServerName: varchar(100), 服务器名称LogTime: datetime, 日志时间
16.3.3 数据库模型
基于上述数据字典,我们可以构建Entity Framework Core的数据模型。以下是主要实体类的定义:
csharp
// 用户和权限模型
public class User
{
public int UserID { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public string Salt { get; set; }
public string Email { get; set; }
public string FullName { get; set; }
public string PhoneNumber { get; set; }
public bool IsActive { get; set; }
public DateTime? LastLoginTime { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public bool IsAdmin { get; set; }
public string AvatarPath { get; set; }
public ICollection<UserRole> UserRoles { get; set; }
}
public class Role
{
public int RoleID { get; set; }
public string RoleName { get; set; }
public string Description { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public ICollection<UserRole> UserRoles { get; set; }
public ICollection<RolePermission> RolePermissions { get; set; }
}
public class Permission
{
public int PermissionID { get; set; }
public string PermissionName { get; set; }
public string Description { get; set; }
public string PermissionCode { get; set; }
public string ModuleCode { get; set; }
public DateTime CreatedTime { get; set; }
public ICollection<RolePermission> RolePermissions { get; set; }
}
public class UserRole
{
public int UserRoleID { get; set; }
public int UserID { get; set; }
public int RoleID { get; set; }
public DateTime AssignedTime { get; set; }
public User User { get; set; }
public Role Role { get; set; }
}
public class RolePermission
{
public int RolePermissionID { get; set; }
public int RoleID { get; set; }
public int PermissionID { get; set; }
public DateTime AssignedTime { get; set; }
public Role Role { get; set; }
public Permission Permission { get; set; }
}
// 服务器资产模型
public class Server
{
public int ServerID { get; set; }
public string Hostname { get; set; }
public string IPAddress { get; set; }
public string MACAddress { get; set; }
public string ServerType { get; set; }
public string OSType { get; set; }
public string OSVersion { get; set; }
public string CPUModel { get; set; }
public int CPUCores { get; set; }
public int MemorySize { get; set; }
public int DiskSize { get; set; }
public string Department { get; set; }
public string Location { get; set; }
public string ResponsiblePerson { get; set; }
public DateTime? PurchaseDate { get; set; }
public DateTime? WarrantyEndDate { get; set; }
public string Status { get; set; }
public string Description { get; set; }
public DateTime? LastCheckTime { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public string AgentStatus { get; set; }
public string AgentVersion { get; set; }
public ICollection<ServerGroupMembership> GroupMemberships { get; set; }
public ICollection<ServerNetworkInterface> NetworkInterfaces { get; set; }
public ICollection<ServerDisk> Disks { get; set; }
public ICollection<Alert> Alerts { get; set; }
public ICollection<MonitorThreshold> Thresholds { get; set; }
public ICollection<TaskTarget> TaskTargets { get; set; }
public ICollection<TaskExecutionDetail> ExecutionDetails { get; set; }
}
// 监控和告警模型
public class MonitorItem
{
public int ItemID { get; set; }
public string ItemName { get; set; }
public string ItemType { get; set; }
public string MetricName { get; set; }
public string Description { get; set; }
public int CollectionInterval { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public ICollection<MonitorThreshold> Thresholds { get; set; }
public ICollection<Alert> Alerts { get; set; }
}
public class Alert
{
public int AlertID { get; set; }
public int ServerID { get; set; }
public int ItemID { get; set; }
public string AlertLevel { get; set; }
public string AlertMessage { get; set; }
public string CurrentValue { get; set; }
public string ThresholdValue { get; set; }
public DateTime FirstOccurTime { get; set; }
public DateTime LastOccurTime { get; set; }
public int OccurrenceCount { get; set; }
public string Status { get; set; }
public int? ProcessedBy { get; set; }
public DateTime? ProcessedTime { get; set; }
public string ProcessingComments { get; set; }
public Server Server { get; set; }
public MonitorItem Item { get; set; }
public User Processor { get; set; }
}
// 任务管理模型
public class Task
{
public int TaskID { get; set; }
public string TaskName { get; set; }
public string TaskType { get; set; }
public string ScriptContent { get; set; }
public string Parameters { get; set; }
public string Description { get; set; }
public int Timeout { get; set; }
public int CreatedBy { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public User Creator { get; set; }
public ICollection<ScheduledTask> Schedules { get; set; }
public ICollection<TaskTarget> Targets { get; set; }
public ICollection<TaskExecution> Executions { get; set; }
}
public class TaskExecution
{
public int ExecutionID { get; set; }
public int TaskID { get; set; }
public int? ScheduleID { get; set; }
public DateTime ExecutionTime { get; set; }
public DateTime? CompletionTime { get; set; }
public string Status { get; set; }
public string TriggerType { get; set; }
public int? TriggeredBy { get; set; }
public int? ParentExecutionID { get; set; }
public Task Task { get; set; }
public ScheduledTask Schedule { get; set; }
public User Trigger { get; set; }
public TaskExecution ParentExecution { get; set; }
public ICollection<TaskExecution> ChildExecutions { get; set; }
public ICollection<TaskExecutionDetail> Details { get; set; }
}
16.4 系统环境部署
系统环境部署是C/S自动化运维平台实施的关键步骤,本节将详细介绍系统的部署环境和方案。
16.4.1 系统环境说明
服务器端环境要求
-
硬件要求:
- CPU:至少4核,推荐8核或更高
- 内存:至少8GB,推荐16GB或更高
- 磁盘:至少200GB,推荐SSD
- 网络:千兆网卡,固定IP地址
-
软件要求:
- 操作系统:Windows Server 2019/2022 或 Linux (如Ubuntu 20.04 LTS)
- .NET:.NET Core 6.0 或更高版本
- 数据库:SQL Server 2019 或 MySQL 8.0
- 时序数据库:InfluxDB 2.0(可选)
- Web服务器:IIS 10 或 Nginx
- 消息队列:RabbitMQ 3.9 或更高版本
- 容器支持:Docker 20.10 或更高版本(可选)
客户端环境要求
-
硬件要求:
- CPU:双核或更高
- 内存:至少4GB
- 磁盘:至少10GB可用空间
- 显示:至少1366x768分辨率
-
软件要求:
- 操作系统:Windows 10/11
- .NET Framework:4.8或更高版本
- 数据库:SQLite(内置)
代理环境要求
-
硬件要求:
- CPU:无特殊要求
- 内存:至少256MB可用内存
- 磁盘:至少200MB可用空间
-
软件要求:
- 操作系统:
- Windows Server 2012 R2/2016/2019/2022
- CentOS/RHEL 7.x/8.x
- Ubuntu 18.04/20.04
- Debian 10/11
- SUSE Linux Enterprise 12/15
- 操作系统:
16.4.2 系统环境搭建
服务器端部署
Windows环境下的部署
-
安装必要组件:
powershell
# 安装IIS Install-WindowsFeature -Name Web-Server -IncludeManagementTools # 安装.NET Hosting Bundle Invoke-WebRequest -Uri https://download.visualstudio.microsoft.com/download/pr/4b8ad533-4464-4b8a-8b11-7f378fa80726/8ce95970db51aae093e3d9233214e2cc/dotnet-hosting-6.0.10-win.exe -OutFile dotnet-hosting.exe Start-Process -FilePath .\dotnet-hosting.exe -ArgumentList '/quiet' -Wait # 安装URL Rewrite模块 Invoke-WebRequest -Uri https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi -OutFile rewrite_amd64.msi Start-Process -FilePath .\rewrite_amd64.msi -ArgumentList '/quiet' -Wait # 重启IIS Restart-Service W3SVC -
安装SQL Server:
- 下载并安装SQL Server 2019 Express或标准版
- 配置SQL Server认证,创建应用程序所需的数据库和用户
-
安装RabbitMQ:
powershell
# 安装Erlang Invoke-WebRequest -Uri https://github.com/erlang/otp/releases/download/OTP-24.1.7/otp_win64_24.1.7.exe -OutFile erlang.exe Start-Process -FilePath .\erlang.exe -ArgumentList '/S' -Wait # 安装RabbitMQ Invoke-WebRequest -Uri https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.13/rabbitmq-server-3.9.13.exe -OutFile rabbitmq.exe Start-Process -FilePath .\rabbitmq.exe -ArgumentList '/S' -Wait # 启用管理插件 & 'C:\Program Files\RabbitMQ Server\rabbitmq_server-3.9.13\sbin\rabbitmq-plugins.bat' enable rabbitmq_management -
部署应用:
powershell
# 创建应用目录 New-Item -ItemType Directory -Path C:\OpsManager # 解压应用文件 Expand-Archive -Path OpsManager-Server.zip -DestinationPath C:\OpsManager # 创建IIS应用程序池 Import-Module WebAdministration New-WebAppPool -Name "OpsManagerAppPool" -Force Set-ItemProperty -Path IIS:\AppPools\OpsManagerAppPool -Name managedRuntimeVersion -Value "" Set-ItemProperty -Path IIS:\AppPools\OpsManagerAppPool -Name processModel.identityType -Value ApplicationPoolIdentity # 创建IIS网站 New-Website -Name "OpsManager" -PhysicalPath C:\OpsManager -ApplicationPool "OpsManagerAppPool" -Port 8080 -Force # 设置应用程序配置 $configPath = "C:\OpsManager\appsettings.json" $config = Get-Content -Path $configPath | ConvertFrom-Json $config.ConnectionStrings.DefaultConnection = "Server=localhost;Database=OpsManager;User Id=opsuser;Password=StrongPassword123!;" $config.ConnectionStrings.InfluxDB = "http://localhost:8086?token=mytoken&org=myorg&bucket=monitoring" $config.RabbitMQ.Host = "localhost" $config.RabbitMQ.Username = "guest" $config.RabbitMQ.Password = "guest" $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath
Linux环境下的部署
-
安装.NET运行时:
bash
# Ubuntu 20.04 wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb sudo apt-get update sudo apt-get install -y apt-transport-https sudo apt-get update sudo apt-get install -y dotnet-sdk-6.0 -
安装MySQL:
bash
sudo apt update sudo apt install -y mysql-server sudo mysql_secure_installation # 创建数据库和用户 sudo mysql -e "CREATE DATABASE OpsManager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" sudo mysql -e "CREATE USER 'opsuser'@'localhost' IDENTIFIED BY 'StrongPassword123!';" sudo mysql -e "GRANT ALL PRIVILEGES ON OpsManager.* TO 'opsuser'@'localhost';" sudo mysql -e "FLUSH PRIVILEGES;" -
安装RabbitMQ:
bash
sudo apt update sudo apt install -y rabbitmq-server sudo systemctl enable rabbitmq-server sudo systemctl start rabbitmq-server sudo rabbitmq-plugins enable rabbitmq_management -
安装Nginx:
bash
sudo apt update sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx -
部署应用:
bash
# 创建应用目录 sudo mkdir -p /opt/OpsManager sudo chown -R $USER:$USER /opt/OpsManager # 解压应用文件 unzip OpsManager-Server.zip -d /opt/OpsManager # 配置应用 cd /opt/OpsManager cat > appsettings.json << EOF { "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=OpsManager;User=opsuser;Password=StrongPassword123!;", "InfluxDB": "http://localhost:8086?token=mytoken&org=myorg&bucket=monitoring" }, "RabbitMQ": { "Host": "localhost", "Username": "guest", "Password": "guest", "VirtualHost": "/" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } EOF # 创建服务 sudo cat > /etc/systemd/system/opsmanager.service << EOF [Unit] Description=OpsManager Service After=network.target [Service] WorkingDirectory=/opt/OpsManager ExecStart=/usr/bin/dotnet /opt/OpsManager/OpsManager.Server.dll Restart=always RestartSec=10 SyslogIdentifier=opsmanager User=www-data Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target EOF sudo systemctl enable opsmanager sudo systemctl start opsmanager -
配置Nginx反向代理:
bash
sudo cat > /etc/nginx/sites-available/opsmanager << EOF server { listen 80; server_name opsmanager.example.com; location / { proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF sudo ln -s /etc/nginx/sites-available/opsmanager /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
客户端部署
客户端的部署相对简单:
-
创建安装包:
- 使用Visual Studio或其他工具创建Windows安装程序(MSI)
- 包含所有必要的依赖项和运行时
- 配置自动更新功能
-
安装步骤:
powershell
# 通过命令行静默安装 msiexec /i OpsManager-Client.msi /quiet SERVER_URL=https://opsmanager.example.com INSTALL_DIR="C:\Program Files\OpsManager" -
配置文件:
xml
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="ServerUrl" value="https://opsmanager.example.com" /> <add key="AutoUpdateEnabled" value="true" /> <add key="UpdateCheckInterval" value="240" /> <add key="LogLevel" value="Info" /> </appSettings> </configuration>
代理部署
代理程序需要部署到每台被管理的服务器上:
Windows服务器
-
创建安装脚本:
powershell
# agent_install.ps1 param ( [string]$ServerUrl = "https://opsmanager.example.com", [string]$AgentKey = "", [string]$InstallDir = "C:\Program Files\OpsManagerAgent" ) # 创建安装目录 if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } # 下载代理程序 $agentUrl = "$ServerUrl/download/agent/windows" $agentZip = "$env:TEMP\opsmanager-agent.zip" Invoke-WebRequest -Uri $agentUrl -OutFile $agentZip # 解压文件 Expand-Archive -Path $agentZip -DestinationPath $InstallDir -Force Remove-Item -Path $agentZip -Force # 创建配置文件 $configPath = "$InstallDir\config.json" $config = @{ ServerUrl = $ServerUrl AgentKey = $AgentKey Hostname = [System.Net.Dns]::GetHostName() LogLevel = "Info" CollectionInterval = 60 HeartbeatInterval = 30 } $config | ConvertTo-Json | Set-Content -Path $configPath # 安装为Windows服务 & "$InstallDir\install-service.ps1" Write-Host "OpsManager Agent installed successfully." -
运行安装脚本:
powershell
# 手动安装 .\agent_install.ps1 -ServerUrl "https://opsmanager.example.com" -AgentKey "your-agent-key" # 或通过组策略批量部署
Linux服务器
-
创建安装脚本:
bash
#!/bin/bash # agent_install.sh # 默认参数 SERVER_URL="https://opsmanager.example.com" AGENT_KEY="" INSTALL_DIR="/opt/opsmanager-agent" # 解析命令行参数 while [[ $# -gt 0 ]]; do key="$1" case $key in --server-url) SERVER_URL="$2" shift shift ;; --agent-key) AGENT_KEY="$2" shift shift ;; --install-dir) INSTALL_DIR="$2" shift shift ;; *) shift ;; esac done # 创建安装目录 mkdir -p $INSTALL_DIR # 下载代理程序 echo "Downloading agent..." if command -v curl > /dev/null; then curl -sSL "$SERVER_URL/download/agent/linux" -o /tmp/opsmanager-agent.tar.gz else wget -q -O /tmp/opsmanager-agent.tar.gz "$SERVER_URL/download/agent/linux" fi # 解压文件 echo "Extracting files..." tar -xzf /tmp/opsmanager-agent.tar.gz -C $INSTALL_DIR rm -f /tmp/opsmanager-agent.tar.gz # 创建配置文件 echo "Creating configuration..." cat > $INSTALL_DIR/config.json << EOF { "ServerUrl": "$SERVER_URL", "AgentKey": "$AGENT_KEY", "Hostname": "$(hostname)", "LogLevel": "Info", "CollectionInterval": 60, "HeartbeatInterval": 30 } EOF # 设置权限 chmod +x $INSTALL_DIR/opsmanager-agent chmod +x $INSTALL_DIR/install-service.sh # 安装服务 echo "Installing service..." $INSTALL_DIR/install-service.sh echo "OpsManager Agent installed successfully." -
运行安装脚本:
bash
# 下载安装脚本 curl -sSL https://opsmanager.example.com/download/install.sh -o install.sh chmod +x install.sh # 运行安装脚本 ./install.sh --server-url https://opsmanager.example.com --agent-key your-agent-key
16.5 系统功能模块设计
系统功能模块是C/S自动化运维平台的核心组成部分,本节将详细介绍各个功能模块的设计和实现。
16.5.1 用户登录模块
用户登录模块是用户访问系统的入口,负责身份验证和会话管理。
登录界面设计
登录界面采用简洁明了的设计风格,包括以下元素:
- 系统标志和名称
- 用户名输入框
- 密码输入框
- 记住登录状态选项
- 登录按钮
- 版本信息
登录功能实现
以下是登录模块的核心代码实现:
csharp
// LoginViewModel.cs
using System;
using System.Security;
using System.Threading.Tasks;
using System.Windows.Input;
using OpsManager.Client.Core;
using OpsManager.Client.Services;
namespace OpsManager.Client.ViewModels
{
public class LoginViewModel : ViewModelBase
{
private readonly IAuthService _authService;
private readonly INavigationService _navigationService;
private readonly ISettingsService _settingsService;
private string _username;
private bool _isLoading;
private string _errorMessage;
private bool _rememberMe;
public string Username
{
get => _username;
set
{
_username = value;
OnPropertyChanged();
}
}
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
// 登录状态改变时,刷新命令可执行状态
LoginCommand.RaiseCanExecuteChanged();
}
}
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
OnPropertyChanged();
}
}
public bool RememberMe
{
get => _rememberMe;
set
{
_rememberMe = value;
OnPropertyChanged();
}
}
public RelayCommand<SecureString> LoginCommand { get; }
public LoginViewModel(
IAuthService authService,
INavigationService navigationService,
ISettingsService settingsService)
{
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
_navigationService = navigationService ?? throw new ArgumentNullException(nameof(navigationService));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
LoginCommand = new RelayCommand<SecureString>(ExecuteLoginAsync, CanLogin);
// 加载保存的用户名
LoadSavedCredentials();
}
private bool CanLogin(SecureString password)
{
return !IsLoading && !string.IsNullOrWhiteSpace(Username) && password != null && password.Length > 0;
}
private async void ExecuteLoginAsync(SecureString password)
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
// 调用认证服务进行登录
var result = await _authService.LoginAsync(Username, password);
if (result.Success)
{
// 保存用户名(如果选择了记住我)
if (RememberMe)
{
_settingsService.SaveSetting("RememberUsername", true);
_settingsService.SaveSetting("Username", Username);
}
else
{
_settingsService.SaveSetting("RememberUsername", false);
_settingsService.SaveSetting("Username", string.Empty);
}
// 导航到主页
_navigationService.NavigateTo("MainView");
}
else
{
ErrorMessage = result.Message;
}
}
catch (Exception ex)
{
ErrorMessage = $"登录时发生错误: {ex.Message}";
Logger.Error(ex, "登录失败");
}
finally
{
IsLoading = false;
}
}
private void LoadSavedCredentials()
{
var rememberUsername = _settingsService.GetSetting<bool>("RememberUsername");
if (rememberUsername)
{
Username = _settingsService.GetSetting<string>("Username");
RememberMe = true;
}
}
}
}
csharp
// AuthService.cs
using System;
using System.Net.Http;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpsManager.Client.Models;
using OpsManager.Client.Utils;
namespace OpsManager.Client.Services
{
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly ISettingsService _settingsService;
private readonly ITokenService _tokenService;
public AuthService(
HttpClient httpClient,
ISettingsService settingsService,
ITokenService tokenService)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
}
public async Task<LoginResult> LoginAsync(string username, SecureString password)
{
try
{
var loginData = new
{
Username = username,
Password = SecureStringHelper.ConvertToUnsecureString(password)
};
var content = new StringContent(
JsonConvert.SerializeObject(loginData),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("api/auth/login", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var loginResponse = JsonConvert.DeserializeObject<LoginResponse>(responseContent);
// 保存令牌
_tokenService.SetToken(loginResponse.Token);
// 保存用户信息
CurrentUser.SetUser(loginResponse.User);
return new LoginResult { Success = true };
}
else
{
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new LoginResult
{
Success = false,
Message = errorResponse?.Message ?? "登录失败,请检查用户名和密码。"
};
}
}
catch (Exception ex)
{
return new LoginResult
{
Success = false,
Message = $"登录过程中发生错误: {ex.Message}"
};
}
}
public void Logout()
{
// 清除令牌
_tokenService.ClearToken();
// 清除用户信息
CurrentUser.Clear();
}
public bool IsAuthenticated()
{
return _tokenService.HasValidToken();
}
}
public class LoginResponse
{
public string Token { get; set; }
public UserInfo User { get; set; }
}
public class ErrorResponse
{
public string Message { get; set; }
}
}
安全性考虑
登录模块采取以下安全措施:
-
密码处理:
- 使用SecureString类型保存内存中的密码
- 密码在传输前才转换为普通字符串
- 登录完成后立即清除内存中的密码
-
传输安全:
- 使用HTTPS加密传输
- 使用TLS 1.2或更高版本
-
认证令牌:
- 使用JWT作为认证令牌
- 令牌包含过期时间
- 令牌安全存储在本地
-
防暴力破解:
- 实现登录失败次数限制
- 多次失败后增加延迟
- 支持账户锁定机制
16.5.2 系统配置功能
系统配置功能允许管理员配置系统的各项参数,包括服务器连接、告警通知、监控参数等。
配置界面设计
配置界面采用分类标签页的设计,包括以下主要分类:
-
基本设置:
- 服务器连接配置
- 客户端显示设置
- 语言和时区设置
-
监控设置:
- 默认监控间隔
- 默认告警阈值
- 数据保留策略
-
通知设置:
- 邮件通知配置
- 短信通知配置
- 企业微信/钉钉配置
-
代理设置:
- 代理部署配置
- 代理升级设置
- 代理安全设置
-
备份设置:
- 备份策略配置
- 备份存储位置
- 自动备份计划
设置存储模型
csharp
// 系统设置模型
public class SystemSettings
{
// 基本设置
public ServerConnectionSettings ServerConnection { get; set; }
public ClientDisplaySettings DisplaySettings { get; set; }
public LocalizationSettings Localization { get; set; }
// 监控设置
public MonitoringSettings Monitoring { get; set; }
// 通知设置
public NotificationSettings Notification { get; set; }
// 代理设置
public AgentSettings Agent { get; set; }
// 备份设置
public BackupSettings Backup { get; set; }
}
// 服务器连接设置
public class ServerConnectionSettings
{
public string ServerUrl { get; set; }
public bool UseHttps { get; set; } = true;
public int ConnectionTimeout { get; set; } = 30;
public int RetryCount { get; set; } = 3;
public int RetryInterval { get; set; } = 5;
}
// 客户端显示设置
public class ClientDisplaySettings
{
public string Theme { get; set; } = "Light";
public int RefreshInterval { get; set; } = 60;
public bool ShowStatusBar { get; set; } = true;
public bool EnableAnimations { get; set; } = true;
public int DefaultPageSize { get; set; } = 20;
}
// 本地化设置
public class LocalizationSettings
{
public string Language { get; set; } = "en-US";
public string TimeZone { get; set; } = "UTC";
public string DateFormat { get; set; } = "yyyy-MM-dd";
public string TimeFormat { get; set; } = "HH:mm:ss";
}
// 监控设置
public class MonitoringSettings
{
public int DefaultCollectionInterval { get; set; } = 60;
public int DataRetentionDays { get; set; } = 90;
public Dictionary<string, ThresholdSetting> DefaultThresholds { get; set; }
}
// 阈值设置
public class ThresholdSetting
{
public double WarningValue { get; set; }
public double CriticalValue { get; set; }
public string ComparisonOperator { get; set; } = ">";
}
// 通知设置
public class NotificationSettings
{
public EmailSettings Email { get; set; }
public SmsSettings Sms { get; set; }
public WebhookSettings Webhook { get; set; }
}
// 邮件设置
public class EmailSettings
{
public bool Enabled { get; set; } = false;
public string SmtpServer { get; set; }
public int SmtpPort { get; set; } = 25;
public bool EnableSsl { get; set; } = true;
public string Username { get; set; }
public string Password { get; set; }
public string FromAddress { get; set; }
public string FromName { get; set; }
}
// 短信设置
public class SmsSettings
{
public bool Enabled { get; set; } = false;
public string Provider { get; set; }
public string ApiKey { get; set; }
public string ApiSecret { get; set; }
public Dictionary<string, string> AdditionalSettings { get; set; }
}
// Webhook设置
public class WebhookSettings
{
public bool Enabled { get; set; } = false;
public List<WebhookEndpoint> Endpoints { get; set; }
}
// Webhook端点
public class WebhookEndpoint
{
public string Name { get; set; }
public string Url { get; set; }
public string Method { get; set; } = "POST";
public Dictionary<string, string> Headers { get; set; }
public string PayloadTemplate { get; set; }
}
// 代理设置
public class AgentSettings
{
public string DefaultInstallPath { get; set; }
public bool AutoUpgrade { get; set; } = true;
public int HeartbeatInterval { get; set; } = 30;
public int CommandTimeout { get; set; } = 60;
public bool EnableCompression { get; set; } = true;
public bool EnableEncryption { get; set; } = true;
}
// 备份设置
public class BackupSettings
{
public bool AutoBackup { get; set; } = true;
public string BackupPath { get; set; }
public int BackupRetentionCount { get; set; } = 10;
public List<BackupSchedule> Schedules { get; set; }
}
// 备份计划
public class BackupSchedule
{
public string Name { get; set; }
public string CronExpression { get; set; }
public bool Enabled { get; set; } = true;
public BackupType Type { get; set; } = BackupType.Full;
}
// 备份类型
public enum BackupType
{
Full,
Incremental,
Differential
}
配置界面实现
csharp
// SettingsViewModel.cs
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using OpsManager.Client.Core;
using OpsManager.Client.Models;
using OpsManager.Client.Services;
namespace OpsManager.Client.ViewModels
{
public class SettingsViewModel : ViewModelBase
{
private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService;
private SystemSettings _settings;
private bool _isLoading;
private bool _isSaving;
private bool _hasChanges;
public SystemSettings Settings
{
get => _settings;
set
{
_settings = value;
OnPropertyChanged();
}
}
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
}
}
public bool IsSaving
{
get => _isSaving;
set
{
_isSaving = value;
OnPropertyChanged();
SaveCommand.RaiseCanExecuteChanged();
}
}
public bool HasChanges
{
get => _hasChanges;
set
{
_hasChanges = value;
OnPropertyChanged();
SaveCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand LoadCommand { get; }
public RelayCommand SaveCommand { get; }
public RelayCommand ResetCommand { get; }
public RelayCommand TestEmailCommand { get; }
public RelayCommand TestSmsCommand { get; }
public SettingsViewModel(
ISettingsService settingsService,
IDialogService dialogService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
LoadCommand = new RelayCommand(ExecuteLoadAsync);
SaveCommand = new RelayCommand(ExecuteSaveAsync, CanSave);
ResetCommand = new RelayCommand(ExecuteReset);
TestEmailCommand = new RelayCommand(ExecuteTestEmailAsync);
TestSmsCommand = new RelayCommand(ExecuteTestSmsAsync);
// 初始化默认设置
Settings = new SystemSettings
{
ServerConnection = new ServerConnectionSettings(),
DisplaySettings = new ClientDisplaySettings(),
Localization = new LocalizationSettings(),
Monitoring = new MonitoringSettings(),
Notification = new NotificationSettings(),
Agent = new AgentSettings(),
Backup = new BackupSettings()
};
// 加载设置
ExecuteLoadAsync();
}
private async void ExecuteLoadAsync()
{
try
{
IsLoading = true;
// 从服务加载设置
var settings = await _settingsService.LoadSystemSettingsAsync();
Settings = settings;
HasChanges = false;
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("加载设置失败", ex.Message);
Logger.Error(ex, "加载系统设置失败");
}
finally
{
IsLoading = false;
}
}
private bool CanSave()
{
return HasChanges && !IsSaving;
}
private async void ExecuteSaveAsync()
{
try
{
IsSaving = true;
// 保存设置到服务
await _settingsService.SaveSystemSettingsAsync(Settings);
HasChanges = false;
await _dialogService.ShowInfoAsync("保存成功", "系统设置已成功保存。");
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("保存设置失败", ex.Message);
Logger.Error(ex, "保存系统设置失败");
}
finally
{
IsSaving = false;
}
}
private void ExecuteReset()
{
// 询问用户确认
_dialogService.ShowConfirmationAsync(
"重置设置",
"确定要重置所有设置到默认值吗?此操作不可撤销。",
confirmed =>
{
if (confirmed)
{
// 重置设置到默认值
Settings = new SystemSettings
{
ServerConnection = new ServerConnectionSettings(),
DisplaySettings = new ClientDisplaySettings(),
Localization = new LocalizationSettings(),
Monitoring = new MonitoringSettings(),
Notification = new NotificationSettings(),
Agent = new AgentSettings(),
Backup = new BackupSettings()
};
HasChanges = true;
}
});
}
private async void ExecuteTestEmailAsync()
{
try
{
await _dialogService.ShowProgressAsync("测试邮件", "正在发送测试邮件...");
// 发送测试邮件
var result = await _settingsService.TestEmailSettingsAsync(Settings.Notification.Email);
await _dialogService.HideProgressAsync();
if (result.Success)
{
await _dialogService.ShowInfoAsync("测试成功", "测试邮件发送成功。");
}
else
{
await _dialogService.ShowErrorAsync("测试失败", result.Message);
}
}
catch (Exception ex)
{
await _dialogService.HideProgressAsync();
await _dialogService.ShowErrorAsync("测试失败", ex.Message);
Logger.Error(ex, "测试邮件设置失败");
}
}
private async void ExecuteTestSmsAsync()
{
try
{
await _dialogService.ShowProgressAsync("测试短信", "正在发送测试短信...");
// 发送测试短信
var result = await _settingsService.TestSmsSettingsAsync(Settings.Notification.Sms);
await _dialogService.HideProgressAsync();
if (result.Success)
{
await _dialogService.ShowInfoAsync("测试成功", "测试短信发送成功。");
}
else
{
await _dialogService.ShowErrorAsync("测试失败", result.Message);
}
}
catch (Exception ex)
{
await _dialogService.HideProgressAsync();
await _dialogService.ShowErrorAsync("测试失败", ex.Message);
Logger.Error(ex, "测试短信设置失败");
}
}
}
}
设置服务实现
csharp
// SettingsService.cs
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpsManager.Client.Models;
namespace OpsManager.Client.Services
{
public class SettingsService : ISettingsService
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorageService;
public SettingsService(
HttpClient httpClient,
ILocalStorageService localStorageService)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_localStorageService = localStorageService ?? throw new ArgumentNullException(nameof(localStorageService));
}
public async Task<SystemSettings> LoadSystemSettingsAsync()
{
try
{
// 先尝试从服务器加载设置
var response = await _httpClient.GetAsync("api/settings/system");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var settings = JsonConvert.DeserializeObject<SystemSettings>(content);
// 保存到本地缓存
await _localStorageService.SetItemAsync("SystemSettings", settings);
return settings;
}
// 如果服务器请求失败,尝试从本地缓存加载
var cachedSettings = await _localStorageService.GetItemAsync<SystemSettings>("SystemSettings");
if (cachedSettings != null)
{
return cachedSettings;
}
// 如果本地缓存也没有,返回默认设置
return new SystemSettings
{
ServerConnection = new ServerConnectionSettings(),
DisplaySettings = new ClientDisplaySettings(),
Localization = new LocalizationSettings(),
Monitoring = new MonitoringSettings(),
Notification = new NotificationSettings(),
Agent = new AgentSettings(),
Backup = new BackupSettings()
};
}
catch (Exception ex)
{
Logger.Error(ex, "加载系统设置失败");
throw;
}
}
public async Task SaveSystemSettingsAsync(SystemSettings settings)
{
try
{
var content = new StringContent(
JsonConvert.SerializeObject(settings),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("api/settings/system", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"保存设置失败: {errorContent}");
}
// 更新本地缓存
await _localStorageService.SetItemAsync("SystemSettings", settings);
}
catch (Exception ex)
{
Logger.Error(ex, "保存系统设置失败");
throw;
}
}
public void SaveSetting<T>(string key, T value)
{
_localStorageService.SetItem(key, value);
}
public T GetSetting<T>(string key, T defaultValue = default)
{
return _localStorageService.GetItem<T>(key, defaultValue);
}
public async Task<TestResult> TestEmailSettingsAsync(EmailSettings settings)
{
try
{
var content = new StringContent(
JsonConvert.SerializeObject(settings),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("api/settings/test-email", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return new TestResult { Success = true };
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new TestResult
{
Success = false,
Message = error?.Message ?? "测试邮件发送失败"
};
}
}
catch (Exception ex)
{
return new TestResult
{
Success = false,
Message = $"测试过程中发生错误: {ex.Message}"
};
}
}
public async Task<TestResult> TestSmsSettingsAsync(SmsSettings settings)
{
try
{
var content = new StringContent(
JsonConvert.SerializeObject(settings),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("api/settings/test-sms", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return new TestResult { Success = true };
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new TestResult
{
Success = false,
Message = error?.Message ?? "测试短信发送失败"
};
}
}
catch (Exception ex)
{
return new TestResult
{
Success = false,
Message = $"测试过程中发生错误: {ex.Message}"
};
}
}
}
public class TestResult
{
public bool Success { get; set; }
public string Message { get; set; }
}
}
16.5.3 服务器分类模块
服务器分类模块用于对服务器进行分类管理,便于批量操作和权限控制。
分类管理界面
服务器分类管理界面包括以下主要功能:
- 分类树形结构显示
- 分类的添加、编辑、删除功能
- 服务器的分配和移除功能
- 拖放操作支持
- 搜索和过滤功能
数据模型设计
csharp
// 服务器分类模型
public class ServerGroup
{
public int GroupID { get; set; }
public string GroupName { get; set; }
public string Description { get; set; }
public int? ParentGroupID { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
// 导航属性
public ServerGroup ParentGroup { get; set; }
public ICollection<ServerGroup> ChildGroups { get; set; }
public ICollection<Server> Servers { get; set; }
}
// 树形节点模型(用于UI显示)
public class TreeNodeModel : ViewModelBase
{
private bool _isExpanded;
private bool _isSelected;
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public NodeType Type { get; set; }
public int? ParentID { get; set; }
public int Count { get; set; }
public bool IsExpanded
{
get => _isExpanded;
set
{
_isExpanded = value;
OnPropertyChanged();
}
}
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
public ObservableCollection<TreeNodeModel> Children { get; } = new ObservableCollection<TreeNodeModel>();
}
public enum NodeType
{
Root,
Group,
Server
}
分类管理实现
csharp
// ServerGroupViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using OpsManager.Client.Core;
using OpsManager.Client.Models;
using OpsManager.Client.Services;
namespace OpsManager.Client.ViewModels
{
public class ServerGroupViewModel : ViewModelBase
{
private readonly IServerGroupService _serverGroupService;
private readonly IServerService _serverService;
private readonly IDialogService _dialogService;
private bool _isLoading;
private string _searchText;
private TreeNodeModel _selectedNode;
private ObservableCollection<TreeNodeModel> _rootNodes;
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
}
}
public string SearchText
{
get => _searchText;
set
{
_searchText = value;
OnPropertyChanged();
ApplySearch();
}
}
public TreeNodeModel SelectedNode
{
get => _selectedNode;
set
{
_selectedNode = value;
OnPropertyChanged();
// 更新命令可执行状态
AddGroupCommand.RaiseCanExecuteChanged();
EditGroupCommand.RaiseCanExecuteChanged();
DeleteGroupCommand.RaiseCanExecuteChanged();
AssignServersCommand.RaiseCanExecuteChanged();
}
}
public ObservableCollection<TreeNodeModel> RootNodes
{
get => _rootNodes;
set
{
_rootNodes = value;
OnPropertyChanged();
}
}
public RelayCommand LoadCommand { get; }
public RelayCommand<TreeNodeModel> SelectNodeCommand { get; }
public RelayCommand AddGroupCommand { get; }
public RelayCommand EditGroupCommand { get; }
public RelayCommand DeleteGroupCommand { get; }
public RelayCommand AssignServersCommand { get; }
public ServerGroupViewModel(
IServerGroupService serverGroupService,
IServerService serverService,
IDialogService dialogService)
{
_serverGroupService = serverGroupService ?? throw new ArgumentNullException(nameof(serverGroupService));
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
RootNodes = new ObservableCollection<TreeNodeModel>();
LoadCommand = new RelayCommand(ExecuteLoadAsync);
SelectNodeCommand = new RelayCommand<TreeNodeModel>(ExecuteSelectNode);
AddGroupCommand = new RelayCommand(ExecuteAddGroupAsync, CanAddGroup);
EditGroupCommand = new RelayCommand(ExecuteEditGroupAsync, CanEditGroup);
DeleteGroupCommand = new RelayCommand(ExecuteDeleteGroupAsync, CanDeleteGroup);
AssignServersCommand = new RelayCommand(ExecuteAssignServersAsync, CanAssignServers);
// 加载分类树
ExecuteLoadAsync();
}
private async void ExecuteLoadAsync()
{
try
{
IsLoading = true;
// 清除现有数据
RootNodes.Clear();
// 加载所有分类
var groups = await _serverGroupService.GetAllGroupsAsync();
// 构建树形结构
var rootNode = new TreeNodeModel
{
ID = 0,
Name = "所有服务器",
Type = NodeType.Root,
IsExpanded = true
};
RootNodes.Add(rootNode);
// 添加顶级分类
var topGroups = groups.Where(g => g.ParentGroupID == null).ToList();
foreach (var group in topGroups)
{
var node = CreateGroupNode(group);
rootNode.Children.Add(node);
// 递归添加子分类
AddChildGroups(node, groups);
}
// 统计每个分类的服务器数量
await UpdateGroupCounts();
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("加载失败", $"加载服务器分类失败: {ex.Message}");
Logger.Error(ex, "加载服务器分类失败");
}
finally
{
IsLoading = false;
}
}
private TreeNodeModel CreateGroupNode(ServerGroup group)
{
return new TreeNodeModel
{
ID = group.GroupID,
Name = group.GroupName,
Description = group.Description,
Type = NodeType.Group,
ParentID = group.ParentGroupID
};
}
private void AddChildGroups(TreeNodeModel parentNode, IEnumerable<ServerGroup> allGroups)
{
var childGroups = allGroups.Where(g => g.ParentGroupID == parentNode.ID).ToList();
foreach (var group in childGroups)
{
var node = CreateGroupNode(group);
parentNode.Children.Add(node);
// 递归处理子分类
AddChildGroups(node, allGroups);
}
}
private async Task UpdateGroupCounts()
{
try
{
// 获取分类的服务器计数
var counts = await _serverGroupService.GetGroupServerCountsAsync();
// 更新分类节点的计数
UpdateNodeCounts(RootNodes, counts);
}
catch (Exception ex)
{
Logger.Error(ex, "更新分类服务器计数失败");
}
}
private void UpdateNodeCounts(IEnumerable<TreeNodeModel> nodes, Dictionary<int, int> counts)
{
foreach (var node in nodes)
{
if (node.Type == NodeType.Group && counts.ContainsKey(node.ID))
{
node.Count = counts[node.ID];
}
// 递归处理子节点
UpdateNodeCounts(node.Children, counts);
}
}
private void ExecuteSelectNode(TreeNodeModel node)
{
if (node != null)
{
// 取消之前选中的节点
if (SelectedNode != null)
{
SelectedNode.IsSelected = false;
}
// 选中当前节点
node.IsSelected = true;
SelectedNode = node;
}
}
private bool CanAddGroup()
{
// 可以添加根级分类,或者在任意分类下添加子分类
return true;
}
private async void ExecuteAddGroupAsync()
{
try
{
// 准备父分类ID
int? parentId = null;
string parentName = "根级";
if (SelectedNode != null && SelectedNode.Type == NodeType.Group)
{
parentId = SelectedNode.ID;
parentName = SelectedNode.Name;
}
// 显示添加分类对话框
var model = new GroupEditModel
{
ParentID = parentId,
ParentName = parentName
};
var result = await _dialogService.ShowGroupEditDialogAsync("添加分类", model);
if (result.Confirmed)
{
// 创建新分类
var newGroup = new ServerGroup
{
GroupName = result.Model.Name,
Description = result.Model.Description,
ParentGroupID = result.Model.ParentID
};
var createdGroup = await _serverGroupService.CreateGroupAsync(newGroup);
// 重新加载分类树
ExecuteLoadAsync();
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("添加失败", $"添加服务器分类失败: {ex.Message}");
Logger.Error(ex, "添加服务器分类失败");
}
}
private bool CanEditGroup()
{
// 只能编辑分类节点
return SelectedNode != null && SelectedNode.Type == NodeType.Group;
}
private async void ExecuteEditGroupAsync()
{
try
{
if (SelectedNode == null || SelectedNode.Type != NodeType.Group)
{
return;
}
// 获取分类详情
var group = await _serverGroupService.GetGroupByIdAsync(SelectedNode.ID);
// 显示编辑分类对话框
var model = new GroupEditModel
{
ID = group.GroupID,
Name = group.GroupName,
Description = group.Description,
ParentID = group.ParentGroupID
};
// 如果有父分类,获取父分类名称
if (group.ParentGroupID.HasValue)
{
var parentGroup = await _serverGroupService.GetGroupByIdAsync(group.ParentGroupID.Value);
model.ParentName = parentGroup?.GroupName ?? "未知";
}
else
{
model.ParentName = "根级";
}
var result = await _dialogService.ShowGroupEditDialogAsync("编辑分类", model);
if (result.Confirmed)
{
// 更新分类
group.GroupName = result.Model.Name;
group.Description = result.Model.Description;
await _serverGroupService.UpdateGroupAsync(group);
// 重新加载分类树
ExecuteLoadAsync();
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("编辑失败", $"编辑服务器分类失败: {ex.Message}");
Logger.Error(ex, "编辑服务器分类失败");
}
}
private bool CanDeleteGroup()
{
// 只能删除分类节点
return SelectedNode != null && SelectedNode.Type == NodeType.Group;
}
private async void ExecuteDeleteGroupAsync()
{
try
{
if (SelectedNode == null || SelectedNode.Type != NodeType.Group)
{
return;
}
// 确认删除
var confirmed = await _dialogService.ShowConfirmationAsync(
"删除分类",
$"确定要删除分类 \"{SelectedNode.Name}\" 吗?其中的服务器将不会被删除,但会从该分类中移除。");
if (confirmed)
{
await _serverGroupService.DeleteGroupAsync(SelectedNode.ID);
// 重新加载分类树
ExecuteLoadAsync();
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("删除失败", $"删除服务器分类失败: {ex.Message}");
Logger.Error(ex, "删除服务器分类失败");
}
}
private bool CanAssignServers()
{
// 只能为分类节点分配服务器
return SelectedNode != null && SelectedNode.Type == NodeType.Group;
}
private async void ExecuteAssignServersAsync()
{
try
{
if (SelectedNode == null || SelectedNode.Type != NodeType.Group)
{
return;
}
// 获取分类中的服务器
var groupServers = await _serverGroupService.GetServersInGroupAsync(SelectedNode.ID);
// 获取所有服务器
var allServers = await _serverService.GetAllServersAsync();
// 准备服务器选择模型
var model = new ServerSelectionModel
{
GroupID = SelectedNode.ID,
GroupName = SelectedNode.Name,
AllServers = allServers.Select(s => new ServerViewModel
{
ID = s.ServerID,
Hostname = s.Hostname,
IPAddress = s.IPAddress,
OSType = s.OSType,
Status = s.Status,
IsSelected = groupServers.Any(gs => gs.ServerID == s.ServerID)
}).ToList()
};
// 显示服务器选择对话框
var result = await _dialogService.ShowServerSelectionDialogAsync("分配服务器", model);
if (result.Confirmed)
{
// 获取选中的服务器ID
var selectedServerIds = result.Model.AllServers
.Where(s => s.IsSelected)
.Select(s => s.ID)
.ToList();
// 更新分类的服务器
await _serverGroupService.UpdateGroupServersAsync(SelectedNode.ID, selectedServerIds);
// 更新分类计数
await UpdateGroupCounts();
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("分配失败", $"分配服务器失败: {ex.Message}");
Logger.Error(ex, "分配服务器到分类失败");
}
}
private void ApplySearch()
{
// 如果搜索文本为空,显示所有节点
if (string.IsNullOrWhiteSpace(SearchText))
{
ShowAllNodes(RootNodes);
return;
}
// 否则根据搜索文本过滤节点
FilterNodes(RootNodes, SearchText.ToLower());
}
private void ShowAllNodes(IEnumerable<TreeNodeModel> nodes)
{
foreach (var node in nodes)
{
node.IsExpanded = false;
if (node.Children.Any())
{
ShowAllNodes(node.Children);
}
}
}
private bool FilterNodes(IEnumerable<TreeNodeModel> nodes, string searchText)
{
bool anyVisible = false;
foreach (var node in nodes)
{
// 检查当前节点是否匹配
bool isMatch = node.Name.ToLower().Contains(searchText) ||
(node.Description?.ToLower().Contains(searchText) ?? false);
// 递归检查子节点
bool hasVisibleChildren = node.Children.Any() && FilterNodes(node.Children, searchText);
// 如果当前节点或其子节点有匹配项,显示节点
if (isMatch || hasVisibleChildren)
{
anyVisible = true;
// 展开包含匹配项的父节点
if (hasVisibleChildren)
{
node.IsExpanded = true;
}
}
}
return anyVisible;
}
}
public class GroupEditModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int? ParentID { get; set; }
public string ParentName { get; set; }
}
public class ServerSelectionModel
{
public int GroupID { get; set; }
public string GroupName { get; set; }
public List<ServerViewModel> AllServers { get; set; }
}
public class ServerViewModel
{
public int ID { get; set; }
public string Hostname { get; set; }
public string IPAddress { get; set; }
public string OSType { get; set; }
public string Status { get; set; }
public bool IsSelected { get; set; }
}
}
16.5.4 系统升级功能
系统升级功能负责客户端和代理程序的版本管理和升级。
升级流程设计
系统升级流程包括以下步骤:
-
版本检测:
- 客户端定期检查服务器是否有新版本
- 代理程序向服务器报告自身版本
-
版本比较:
- 比较本地版本和服务器版本
- 判断是否需要升级
-
下载升级包:
- 下载适用于当前系统的升级包
- 验证下载包的完整性和签名
-
安装升级:
- 备份当前版本
- 安装新版本
- 迁移配置和数据
-
升级验证:
- 验证升级是否成功
- 报告升级结果
客户端升级实现
csharp
// UpdateService.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpsManager.Client.Models;
using OpsManager.Client.Utils;
namespace OpsManager.Client.Services
{
public class UpdateService : IUpdateService
{
private readonly HttpClient _httpClient;
private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService;
public UpdateService(
HttpClient httpClient,
ISettingsService settingsService,
IDialogService dialogService)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
}
public async Task<UpdateCheckResult> CheckForUpdatesAsync()
{
try
{
// 获取当前版本
var currentVersion = AppInfo.Version;
// 请求服务器版本信息
var response = await _httpClient.GetAsync("api/updates/check?version=" + currentVersion);
if (!response.IsSuccessStatusCode)
{
return new UpdateCheckResult
{
HasUpdate = false,
Message = "检查更新失败,服务器返回错误。"
};
}
var content = await response.Content.ReadAsStringAsync();
var updateInfo = JsonConvert.DeserializeObject<UpdateInfo>(content);
// 比较版本
if (updateInfo.HasUpdate)
{
return new UpdateCheckResult
{
HasUpdate = true,
LatestVersion = updateInfo.Version,
ReleaseNotes = updateInfo.ReleaseNotes,
DownloadUrl = updateInfo.DownloadUrl,
IsRequired = updateInfo.IsRequired,
PublishDate = updateInfo.PublishDate,
FileSize = updateInfo.FileSize,
Checksum = updateInfo.Checksum
};
}
return new UpdateCheckResult
{
HasUpdate = false,
Message = "当前版本已是最新。"
};
}
catch (Exception ex)
{
Logger.Error(ex, "检查更新失败");
return new UpdateCheckResult
{
HasUpdate = false,
Message = $"检查更新失败: {ex.Message}"
};
}
}
public async Task<UpdateResult> DownloadAndInstallUpdateAsync(UpdateCheckResult updateInfo, IProgress<ProgressInfo> progress)
{
try
{
// 报告进度:开始下载
progress?.Report(new ProgressInfo { Status = "正在下载更新...", Percentage = 0 });
// 创建临时目录
var tempDir = Path.Combine(Path.GetTempPath(), "OpsManagerUpdate");
Directory.CreateDirectory(tempDir);
// 下载文件路径
var downloadPath = Path.Combine(tempDir, "update.zip");
// 下载更新文件
using (var response = await _httpClient.GetAsync(updateInfo.DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? 0;
var buffer = new byte[8192];
var bytesRead = 0L;
using (var fileStream = new FileStream(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None))
using (var stream = await response.Content.ReadAsStreamAsync())
{
int read;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, read);
bytesRead += read;
var percentage = totalBytes > 0 ? (int)(bytesRead * 100 / totalBytes) : 0;
// 报告下载进度
progress?.Report(new ProgressInfo
{
Status = $"正在下载更新... {BytesFormatter.Format(bytesRead)} / {BytesFormatter.Format(totalBytes)}",
Percentage = percentage
});
}
}
}
// 报告进度:验证文件
progress?.Report(new ProgressInfo { Status = "正在验证文件...", Percentage = 50 });
// 验证文件完整性
if (!await VerifyFileChecksumAsync(downloadPath, updateInfo.Checksum))
{
return new UpdateResult
{
Success = false,
Message = "文件验证失败,可能已损坏。"
};
}
// 报告进度:准备安装
progress?.Report(new ProgressInfo { Status = "正在准备安装...", Percentage = 60 });
// 解压更新文件
var extractPath = Path.Combine(tempDir, "extracted");
Directory.CreateDirectory(extractPath);
System.IO.Compression.ZipFile.ExtractToDirectory(downloadPath, extractPath);
// 报告进度:安装更新
progress?.Report(new ProgressInfo { Status = "正在安装更新...", Percentage = 70 });
// 准备更新器程序路径
var updaterPath = Path.Combine(extractPath, "Updater.exe");
if (!File.Exists(updaterPath))
{
return new UpdateResult
{
Success = false,
Message = "更新包不完整,缺少更新器程序。"
};
}
// 创建参数
var arguments = $"--app-path \"{AppInfo.ExecutablePath}\" --update-path \"{extractPath}\" --version \"{updateInfo.LatestVersion}\"";
// 启动更新器
var processStartInfo = new ProcessStartInfo
{
FileName = updaterPath,
Arguments = arguments,
UseShellExecute = true
};
Process.Start(processStartInfo);
// 关闭当前应用程序
progress?.Report(new ProgressInfo { Status = "更新器已启动,应用程序将重启...", Percentage = 100 });
// 返回成功结果
return new UpdateResult
{
Success = true,
Message = "更新器已启动,应用程序将重启。"
};
}
catch (Exception ex)
{
Logger.Error(ex, "下载或安装更新失败");
return new UpdateResult
{
Success = false,
Message = $"更新失败: {ex.Message}"
};
}
}
private async Task<bool> VerifyFileChecksumAsync(string filePath, string expectedChecksum)
{
try
{
using (var stream = File.OpenRead(filePath))
using (var sha256 = SHA256.Create())
{
var hashBytes = await sha256.ComputeHashAsync(stream);
var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
return string.Equals(hash, expectedChecksum, StringComparison.OrdinalIgnoreCase);
}
}
catch (Exception ex)
{
Logger.Error(ex, "验证文件校验和失败");
return false;
}
}
}
public class UpdateCheckResult
{
public bool HasUpdate { get; set; }
public string LatestVersion { get; set; }
public string ReleaseNotes { get; set; }
public string DownloadUrl { get; set; }
public bool IsRequired { get; set; }
public DateTime PublishDate { get; set; }
public long FileSize { get; set; }
public string Checksum { get; set; }
public string Message { get; set; }
}
public class UpdateResult
{
public bool Success { get; set; }
public string Message { get; set; }
}
public class UpdateInfo
{
public bool HasUpdate { get; set; }
public string Version { get; set; }
public string ReleaseNotes { get; set; }
public string DownloadUrl { get; set; }
public bool IsRequired { get; set; }
public DateTime PublishDate { get; set; }
public long FileSize { get; set; }
public string Checksum { get; set; }
}
public class ProgressInfo
{
public string Status { get; set; }
public int Percentage { get; set; }
}
}
代理升级实现
csharp
// AgentUpgradeService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpsManager.Client.Models;
using OpsManager.Client.Utils;
namespace OpsManager.Client.Services
{
public class AgentUpgradeService : IAgentUpgradeService
{
private readonly HttpClient _httpClient;
private readonly IDialogService _dialogService;
public AgentUpgradeService(
HttpClient httpClient,
IDialogService dialogService)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
}
public async Task<IEnumerable<AgentUpgradeInfo>> GetAgentsNeedingUpgradeAsync()
{
try
{
var response = await _httpClient.GetAsync("api/agents/needing-upgrade");
if (!response.IsSuccessStatusCode)
{
return Enumerable.Empty<AgentUpgradeInfo>();
}
var content = await response.Content.ReadAsStringAsync();
var agents = JsonConvert.DeserializeObject<List<AgentUpgradeInfo>>(content);
return agents;
}
catch (Exception ex)
{
Logger.Error(ex, "获取需要升级的代理失败");
return Enumerable.Empty<AgentUpgradeInfo>();
}
}
public async Task<UpgradeResult> UpgradeAgentsAsync(IEnumerable<int> serverIds, IProgress<ProgressInfo> progress)
{
try
{
// 报告进度:开始升级
progress?.Report(new ProgressInfo { Status = "正在准备升级代理...", Percentage = 0 });
var serverIdList = serverIds.ToList();
// 获取服务器详情
var serversResponse = await _httpClient.PostAsync(
"api/servers/batch",
new StringContent(JsonConvert.SerializeObject(serverIdList), Encoding.UTF8, "application/json"));
if (!serversResponse.IsSuccessStatusCode)
{
return new UpgradeResult
{
Success = false,
Message = "获取服务器信息失败"
};
}
var serversContent = await serversResponse.Content.ReadAsStringAsync();
var servers = JsonConvert.DeserializeObject<List<Server>>(serversContent);
// 报告进度:开始升级
progress?.Report(new ProgressInfo
{
Status = $"正在升级 {servers.Count} 台服务器的代理...",
Percentage = 10
});
// 发送升级请求
var upgradeResponse = await _httpClient.PostAsync(
"api/agents/upgrade",
new StringContent(JsonConvert.SerializeObject(serverIdList), Encoding.UTF8, "application/json"));
if (!upgradeResponse.IsSuccessStatusCode)
{
var errorContent = await upgradeResponse.Content.ReadAsStringAsync();
return new UpgradeResult
{
Success = false,
Message = $"升级请求失败: {errorContent}"
};
}
var upgradeContent = await upgradeResponse.Content.ReadAsStringAsync();
var upgradeResult = JsonConvert.DeserializeObject<UpgradeTaskResult>(upgradeContent);
// 报告进度:等待升级结果
progress?.Report(new ProgressInfo
{
Status = "升级任务已提交,正在等待结果...",
Percentage = 30
});
// 轮询升级任务状态
var taskId = upgradeResult.TaskId;
var isCompleted = false;
var totalSteps = 10;
var currentStep = 0;
while (!isCompleted && currentStep < totalSteps)
{
// 等待一段时间
await Task.Delay(3000);
currentStep++;
// 查询任务状态
var statusResponse = await _httpClient.GetAsync($"api/tasks/{taskId}/status");
if (statusResponse.IsSuccessStatusCode)
{
var statusContent = await statusResponse.Content.ReadAsStringAsync();
var taskStatus = JsonConvert.DeserializeObject<TaskStatus>(statusContent);
if (taskStatus.IsCompleted)
{
isCompleted = true;
// 报告进度:完成
progress?.Report(new ProgressInfo
{
Status = "代理升级已完成",
Percentage = 100
});
return new UpgradeResult
{
Success = true,
Message = $"已成功升级 {taskStatus.SuccessCount} 台服务器的代理,失败 {taskStatus.FailureCount} 台",
SuccessCount = taskStatus.SuccessCount,
FailureCount = taskStatus.FailureCount,
Details = taskStatus.Details
};
}
else
{
// 报告进度:进行中
var percentage = 30 + (currentStep * 70 / totalSteps);
progress?.Report(new ProgressInfo
{
Status = $"正在升级代理 ({taskStatus.SuccessCount}/{serverIdList.Count} 完成)...",
Percentage = percentage
});
}
}
}
// 如果超时,返回未完成状态
return new UpgradeResult
{
Success = true,
Message = "代理升级任务已提交,但尚未完成。请稍后检查结果。",
TaskId = taskId
};
}
catch (Exception ex)
{
Logger.Error(ex, "升级代理失败");
return new UpgradeResult
{
Success = false,
Message = $"升级代理时发生错误: {ex.Message}"
};
}
}
}
public class AgentUpgradeInfo
{
public int ServerID { get; set; }
public string Hostname { get; set; }
public string IPAddress { get; set; }
public string CurrentVersion { get; set; }
public string LatestVersion { get; set; }
public string OSType { get; set; }
public string Status { get; set; }
}
public class UpgradeResult
{
public bool Success { get; set; }
public string Message { get; set; }
public int SuccessCount { get; set; }
public int FailureCount { get; set; }
public string TaskId { get; set; }
public List<string> Details { get; set; }
}
public class UpgradeTaskResult
{
public string TaskId { get; set; }
public string Message { get; set; }
}
public class TaskStatus
{
public bool IsCompleted { get; set; }
public int SuccessCount { get; set; }
public int FailureCount { get; set; }
public List<string> Details { get; set; }
}
}
16.5.5 客户端模块编写
客户端模块是用户与系统交互的界面,采用WPF技术实现,支持丰富的交互体验。
主窗口设计
主窗口包含以下主要元素:
- 顶部菜单栏和工具栏
- 左侧导航菜单
- 主内容区域
- 底部状态栏
xaml
<!-- MainWindow.xaml -->
<Window x:Class="OpsManager.Client.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:OpsManager.Client.Views"
xmlns:vm="clr-namespace:OpsManager.Client.ViewModels"
xmlns:controls="clr-namespace:OpsManager.Client.Controls"
mc:Ignorable="d"
Title="{Binding WindowTitle}"
Height="768" Width="1024"
MinHeight="600" MinWidth="800"
Icon="/Resources/Images/app_icon.ico"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles/MainStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 顶部菜单和工具栏 -->
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 主菜单 -->
<Menu Grid.Row="0" Style="{StaticResource MainMenu}">
<MenuItem Header="文件">
<MenuItem Header="导出数据" Command="{Binding ExportDataCommand}"/>
<MenuItem Header="导入数据" Command="{Binding ImportDataCommand}"/>
<Separator/>
<MenuItem Header="打印" Command="{Binding PrintCommand}"/>
<Separator/>
<MenuItem Header="退出" Command="{Binding ExitCommand}"/>
</MenuItem>
<MenuItem Header="视图">
<MenuItem Header="刷新" Command="{Binding RefreshCommand}" InputGestureText="F5"/>
<MenuItem Header="全屏" Command="{Binding ToggleFullScreenCommand}" InputGestureText="F11"/>
<Separator/>
<MenuItem Header="状态栏" IsCheckable="True" IsChecked="{Binding ShowStatusBar}"/>
</MenuItem>
<MenuItem Header="工具">
<MenuItem Header="系统设置" Command="{Binding OpenSettingsCommand}"/>
<MenuItem Header="检查更新" Command="{Binding CheckUpdateCommand}"/>
<Separator/>
<MenuItem Header="日志查看器" Command="{Binding OpenLogViewerCommand}"/>
</MenuItem>
<MenuItem Header="帮助">
<MenuItem Header="帮助文档" Command="{Binding OpenHelpCommand}"/>
<MenuItem Header="在线支持" Command="{Binding OpenSupportCommand}"/>
<Separator/>
<MenuItem Header="关于" Command="{Binding OpenAboutCommand}"/>
</MenuItem>
</Menu>
<!-- 工具栏 -->
<ToolBar Grid.Row="1" Style="{StaticResource MainToolBar}">
<Button Command="{Binding RefreshCommand}" ToolTip="刷新">
<Image Source="/Resources/Images/refresh.png" Width="16" Height="16"/>
</Button>
<Separator/>
<Button Command="{Binding AddServerCommand}" ToolTip="添加服务器">
<Image Source="/Resources/Images/add_server.png" Width="16" Height="16"/>
</Button>
<Button Command="{Binding ManageGroupsCommand}" ToolTip="管理分组">
<Image Source="/Resources/Images/groups.png" Width="16" Height="16"/>
</Button>
<Separator/>
<Button Command="{Binding StartTaskCommand}" ToolTip="执行任务">
<Image Source="/Resources/Images/task_run.png" Width="16" Height="16"/>
</Button>
<Button Command="{Binding CreateTaskCommand}" ToolTip="创建任务">
<Image Source="/Resources/Images/task_new.png" Width="16" Height="16"/>
</Button>
<Separator/>
<Button Command="{Binding OpenSettingsCommand}" ToolTip="系统设置">
<Image Source="/Resources/Images/settings.png" Width="16" Height="16"/>
</Button>
<Button Command="{Binding OpenHelpCommand}" ToolTip="帮助">
<Image Source="/Resources/Images/help.png" Width="16" Height="16"/>
</Button>
</ToolBar>
</Grid>
<!-- 主内容区 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" MinWidth="200"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧导航菜单 -->
<Grid Grid.Column="0" Background="{StaticResource NavigationBackground}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 用户信息 -->
<Border Grid.Row="0" Padding="10" BorderThickness="0 0 0 1" BorderBrush="{StaticResource BorderBrush}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Width="40" Height="40" Margin="0 0 10 0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding UserAvatar}"/>
</Ellipse.Fill>
</Ellipse>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding UserName}" FontWeight="Bold"/>
<TextBlock Text="{Binding UserRole}" Foreground="{StaticResource SubtleBrush}"/>
</StackPanel>
</Grid>
</Border>
<!-- 导航菜单 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding NavigationItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:NavigationItem
Icon="{Binding Icon}"
Title="{Binding Title}"
IsSelected="{Binding IsSelected}"
Command="{Binding Command}"
Badge="{Binding Badge}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<!-- 分隔线 -->
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="Transparent"/>
<!-- 内容区域 -->
<Border Grid.Column="2" Background="{StaticResource ContentBackground}" Padding="10">
<ContentControl Content="{Binding CurrentView}"/>
</Border>
</Grid>
<!-- 底部状态栏 -->
<StatusBar Grid.Row="2" Visibility="{Binding ShowStatusBar, Converter={StaticResource BooleanToVisibilityConverter}}">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<!-- 连接状态 -->
<StatusBarItem Grid.Column="0">
<StackPanel Orientation="Horizontal">
<Ellipse Width="10" Height="10" Margin="0 0 5 0" Fill="{Binding ConnectionStatus, Converter={StaticResource ConnectionStatusToColorConverter}}"/>
<TextBlock Text="{Binding ConnectionStatusText}"/>
</StackPanel>
</StatusBarItem>
<!-- 消息 -->
<StatusBarItem Grid.Column="1">
<TextBlock Text="{Binding StatusMessage}"/>
</StatusBarItem>
<!-- 服务器计数 -->
<StatusBarItem Grid.Column="2">
<TextBlock Text="{Binding ServerStatusSummary}"/>
</StatusBarItem>
<!-- 版本信息 -->
<StatusBarItem Grid.Column="3">
<TextBlock Text="{Binding VersionInfo}"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
主窗口视图模型
csharp
// MainWindowViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using OpsManager.Client.Core;
using OpsManager.Client.Models;
using OpsManager.Client.Services;
using OpsManager.Client.Utils;
using OpsManager.Client.Views;
namespace OpsManager.Client.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private readonly INavigationService _navigationService;
private readonly IAuthService _authService;
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private readonly IServerService _serverService;
private string _windowTitle;
private string _userName;
private string _userRole;
private string _userAvatar;
private object _currentView;
private bool _showStatusBar;
private ConnectionStatus _connectionStatus;
private string _statusMessage;
private string _serverStatusSummary;
private string _versionInfo;
public string WindowTitle
{
get => _windowTitle;
set
{
_windowTitle = value;
OnPropertyChanged();
}
}
public string UserName
{
get => _userName;
set
{
_userName = value;
OnPropertyChanged();
}
}
public string UserRole
{
get => _userRole;
set
{
_userRole = value;
OnPropertyChanged();
}
}
public string UserAvatar
{
get => _userAvatar;
set
{
_userAvatar = value;
OnPropertyChanged();
}
}
public object CurrentView
{
get => _currentView;
set
{
_currentView = value;
OnPropertyChanged();
}
}
public bool ShowStatusBar
{
get => _showStatusBar;
set
{
_showStatusBar = value;
OnPropertyChanged();
_settingsService.SaveSetting("ShowStatusBar", value);
}
}
public ConnectionStatus ConnectionStatus
{
get => _connectionStatus;
set
{
_connectionStatus = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ConnectionStatusText));
}
}
public string ConnectionStatusText
{
get
{
return ConnectionStatus switch
{
ConnectionStatus.Connected => "已连接",
ConnectionStatus.Connecting => "正在连接...",
ConnectionStatus.Disconnected => "未连接",
_ => "未知状态"
};
}
}
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
OnPropertyChanged();
}
}
public string ServerStatusSummary
{
get => _serverStatusSummary;
set
{
_serverStatusSummary = value;
OnPropertyChanged();
}
}
public string VersionInfo
{
get => _versionInfo;
set
{
_versionInfo = value;
OnPropertyChanged();
}
}
public ObservableCollection<NavigationItemViewModel> NavigationItems { get; } = new ObservableCollection<NavigationItemViewModel>();
public ICommand RefreshCommand { get; }
public ICommand ExportDataCommand { get; }
public ICommand ImportDataCommand { get; }
public ICommand PrintCommand { get; }
public ICommand ExitCommand { get; }
public ICommand ToggleFullScreenCommand { get; }
public ICommand OpenSettingsCommand { get; }
public ICommand CheckUpdateCommand { get; }
public ICommand OpenLogViewerCommand { get; }
public ICommand OpenHelpCommand { get; }
public ICommand OpenSupportCommand { get; }
public ICommand OpenAboutCommand { get; }
public ICommand AddServerCommand { get; }
public ICommand ManageGroupsCommand { get; }
public ICommand StartTaskCommand { get; }
public ICommand CreateTaskCommand { get; }
public MainWindowViewModel(
INavigationService navigationService,
IAuthService authService,
IDialogService dialogService,
ISettingsService settingsService,
IUpdateService updateService,
IServerService serverService)
{
_navigationService = navigationService ?? throw new ArgumentNullException(nameof(navigationService));
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_updateService = updateService ?? throw new ArgumentNullException(nameof(updateService));
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
// 初始化命令
RefreshCommand = new RelayCommand(ExecuteRefresh);
ExportDataCommand = new RelayCommand(ExecuteExportData);
ImportDataCommand = new RelayCommand(ExecuteImportData);
PrintCommand = new RelayCommand(ExecutePrint);
ExitCommand = new RelayCommand(ExecuteExit);
ToggleFullScreenCommand = new RelayCommand(ExecuteToggleFullScreen);
OpenSettingsCommand = new RelayCommand(ExecuteOpenSettings);
CheckUpdateCommand = new RelayCommand(ExecuteCheckUpdateAsync);
OpenLogViewerCommand = new RelayCommand(ExecuteOpenLogViewer);
OpenHelpCommand = new RelayCommand(ExecuteOpenHelp);
OpenSupportCommand = new RelayCommand(ExecuteOpenSupport);
OpenAboutCommand = new RelayCommand(ExecuteOpenAbout);
AddServerCommand = new RelayCommand(ExecuteAddServer);
ManageGroupsCommand = new RelayCommand(ExecuteManageGroups);
StartTaskCommand = new RelayCommand(ExecuteStartTask);
CreateTaskCommand = new RelayCommand(ExecuteCreateTask);
// 初始化导航菜单
InitializeNavigationItems();
// 设置初始视图
NavigateTo("Dashboard");
// 初始化窗口标题
WindowTitle = $"运维管理平台 - {AppInfo.Version}";
// 加载用户信息
LoadUserInfo();
// 加载设置
LoadSettings();
// 设置版本信息
VersionInfo = $"版本 {AppInfo.Version}";
// 检查服务器状态
UpdateServerStatus();
// 检查连接状态
CheckConnectionStatus();
// 自动检查更新
CheckForUpdatesAsync();
}
private void InitializeNavigationItems()
{
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Dashboard",
Title = "仪表板",
ViewName = "Dashboard",
Command = new RelayCommand(() => NavigateTo("Dashboard"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Servers",
Title = "服务器管理",
ViewName = "Servers",
Command = new RelayCommand(() => NavigateTo("Servers"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Monitoring",
Title = "监控管理",
ViewName = "Monitoring",
Command = new RelayCommand(() => NavigateTo("Monitoring"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "RemoteControl",
Title = "远程控制",
ViewName = "RemoteControl",
Command = new RelayCommand(() => NavigateTo("RemoteControl"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Task",
Title = "任务管理",
ViewName = "Tasks",
Command = new RelayCommand(() => NavigateTo("Tasks"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Software",
Title = "软件管理",
ViewName = "Software",
Command = new RelayCommand(() => NavigateTo("Software"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Reports",
Title = "报表中心",
ViewName = "Reports",
Command = new RelayCommand(() => NavigateTo("Reports"))
});
NavigationItems.Add(new NavigationItemViewModel
{
Icon = "Settings",
Title = "系统设置",
ViewName = "Settings",
Command = new RelayCommand(() => NavigateTo("Settings"))
});
}
private void NavigateTo(string viewName)
{
// 更新导航项选中状态
foreach (var item in NavigationItems)
{
item.IsSelected = item.ViewName == viewName;
}
// 使用导航服务切换视图
CurrentView = _navigationService.GetView(viewName);
// 更新状态消息
StatusMessage = $"已切换到{GetViewTitle(viewName)}";
}
private string GetViewTitle(string viewName)
{
return viewName switch
{
"Dashboard" => "仪表板",
"Servers" => "服务器管理",
"Monitoring" => "监控管理",
"RemoteControl" => "远程控制",
"Tasks" => "任务管理",
"Software" => "软件管理",
"Reports" => "报表中心",
"Settings" => "系统设置",
_ => viewName
};
}
private void LoadUserInfo()
{
var currentUser = CurrentUser.Instance;
UserName = currentUser.FullName;
UserRole = currentUser.RoleName;
UserAvatar = currentUser.AvatarUrl ?? "/Resources/Images/default_avatar.png";
}
private void LoadSettings()
{
ShowStatusBar = _settingsService.GetSetting("ShowStatusBar", true);
}
private async void UpdateServerStatus()
{
try
{
var summary = await _serverService.GetServerStatusSummaryAsync();
ServerStatusSummary = $"服务器: {summary.TotalCount} 总数, {summary.OnlineCount} 在线, {summary.OfflineCount} 离线, {summary.WarningCount} 警告";
}
catch (Exception ex)
{
Logger.Error(ex, "获取服务器状态摘要失败");
ServerStatusSummary = "服务器状态: 未知";
}
}
private async void CheckConnectionStatus()
{
try
{
ConnectionStatus = ConnectionStatus.Connecting;
var isConnected = await _authService.CheckConnectionAsync();
ConnectionStatus = isConnected ? ConnectionStatus.Connected : ConnectionStatus.Disconnected;
}
catch (Exception ex)
{
Logger.Error(ex, "检查连接状态失败");
ConnectionStatus = ConnectionStatus.Disconnected;
}
}
private async void CheckForUpdatesAsync()
{
try
{
var autoCheck = _settingsService.GetSetting("AutoCheckUpdates", true);
if (!autoCheck)
{
return;
}
var result = await _updateService.CheckForUpdatesAsync();
if (result.HasUpdate)
{
// 如果发现更新,显示通知
var message = $"发现新版本 {result.LatestVersion}。是否立即更新?";
var confirmed = await _dialogService.ShowConfirmationAsync("更新可用", message);
if (confirmed)
{
await DownloadAndInstallUpdateAsync(result);
}
}
}
catch (Exception ex)
{
Logger.Error(ex, "自动检查更新失败");
}
}
private async Task DownloadAndInstallUpdateAsync(UpdateCheckResult updateInfo)
{
try
{
var progress = new Progress<ProgressInfo>(info =>
{
StatusMessage = info.Status;
});
var result = await _updateService.DownloadAndInstallUpdateAsync(updateInfo, progress);
if (!result.Success)
{
await _dialogService.ShowErrorAsync("更新失败", result.Message);
}
}
catch (Exception ex)
{
Logger.Error(ex, "下载和安装更新失败");
await _dialogService.ShowErrorAsync("更新失败", $"无法完成更新: {ex.Message}");
}
}
private void ExecuteRefresh()
{
// 刷新当前视图
var refreshable = CurrentView as IRefreshable;
refreshable?.Refresh();
// 更新服务器状态
UpdateServerStatus();
// 检查连接状态
CheckConnectionStatus();
StatusMessage = "已刷新";
}
private void ExecuteExportData()
{
// 实现导出数据功能
}
private void ExecuteImportData()
{
// 实现导入数据功能
}
private void ExecutePrint()
{
// 实现打印功能
}
private void ExecuteExit()
{
// 退出应用程序
App.Current.Shutdown();
}
private void ExecuteToggleFullScreen()
{
// 实现全屏切换功能
}
private void ExecuteOpenSettings()
{
NavigateTo("Settings");
}
private async void ExecuteCheckUpdateAsync()
{
try
{
StatusMessage = "正在检查更新...";
var result = await _updateService.CheckForUpdatesAsync();
if (result.HasUpdate)
{
var message = $"发现新版本 {result.LatestVersion}。是否立即更新?\n\n发布日期: {result.PublishDate:yyyy-MM-dd}\n大小: {BytesFormatter.Format(result.FileSize)}\n\n更新内容:\n{result.ReleaseNotes}";
var confirmed = await _dialogService.ShowConfirmationAsync("更新可用", message);
if (confirmed)
{
await DownloadAndInstallUpdateAsync(result);
}
}
else
{
await _dialogService.ShowInfoAsync("检查更新", "当前已是最新版本。");
StatusMessage = "检查更新完成";
}
}
catch (Exception ex)
{
Logger.Error(ex, "检查更新失败");
await _dialogService.ShowErrorAsync("检查更新失败", ex.Message);
StatusMessage = "检查更新失败";
}
}
private void ExecuteOpenLogViewer()
{
// 打开日志查看器
}
private void ExecuteOpenHelp()
{
// 打开帮助文档
ProcessHelper.OpenUrl("https://example.com/help");
}
private void ExecuteOpenSupport()
{
// 打开在线支持
ProcessHelper.OpenUrl("https://example.com/support");
}
private void ExecuteOpenAbout()
{
// 显示关于对话框
_dialogService.ShowDialog(new AboutView());
}
private void ExecuteAddServer()
{
// 打开添加服务器对话框
}
private void ExecuteManageGroups()
{
// 打开管理分组对话框
}
private void ExecuteStartTask()
{
// 打开执行任务对话框
}
private void ExecuteCreateTask()
{
// 打开创建任务对话框
}
}
public class NavigationItemViewModel : ViewModelBase
{
private bool _isSelected;
private int _badge;
public string Icon { get; set; }
public string Title { get; set; }
public string ViewName { get; set; }
public ICommand Command { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
public int Badge
{
get => _badge;
set
{
_badge = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowBadge));
}
}
public bool ShowBadge => Badge > 0;
}
public enum ConnectionStatus
{
Connected,
Connecting,
Disconnected
}
}
16.5.6 执行功能模块
执行功能模块是系统的核心功能之一,负责在远程服务器上执行命令和脚本。
远程命令执行
csharp
// RemoteExecutionService.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpsManager.Client.Models;
namespace OpsManager.Client.Services
{
public class RemoteExecutionService : IRemoteExecutionService
{
private readonly HttpClient _httpClient;
public RemoteExecutionService(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<CommandResult> ExecuteCommandAsync(
int serverId,
string command,
int timeoutSeconds = 60,
CancellationToken cancellationToken = default)
{
try
{
var requestData = new
{
Command = command,
Timeout = timeoutSeconds
};
var content = new StringContent(
JsonConvert.SerializeObject(requestData),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync(
$"api/servers/{serverId}/execute",
content,
cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<CommandResult>(responseContent);
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new CommandResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = error?.Message ?? "执行命令失败"
};
}
}
catch (TaskCanceledException)
{
return new CommandResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = "命令执行已取消"
};
}
catch (Exception ex)
{
return new CommandResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = $"执行命令时发生错误: {ex.Message}"
};
}
}
public async Task<ScriptResult> ExecuteScriptAsync(
int serverId,
string scriptContent,
string scriptType = "bash",
Dictionary<string, string> parameters = null,
int timeoutSeconds = 300,
CancellationToken cancellationToken = default)
{
try
{
var requestData = new
{
ScriptContent = scriptContent,
ScriptType = scriptType,
Parameters = parameters ?? new Dictionary<string, string>(),
Timeout = timeoutSeconds
};
var content = new StringContent(
JsonConvert.SerializeObject(requestData),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync(
$"api/servers/{serverId}/execute-script",
content,
cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<ScriptResult>(responseContent);
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new ScriptResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = error?.Message ?? "执行脚本失败",
ExecutionTime = 0
};
}
}
catch (TaskCanceledException)
{
return new ScriptResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = "脚本执行已取消",
ExecutionTime = 0
};
}
catch (Exception ex)
{
return new ScriptResult
{
Success = false,
ExitCode = -1,
Output = string.Empty,
Error = $"执行脚本时发生错误: {ex.Message}",
ExecutionTime = 0
};
}
}
public async Task<BatchCommandResult> ExecuteBatchCommandAsync(
IEnumerable<int> serverIds,
string command,
int timeoutSeconds = 60,
bool parallel = true,
CancellationToken cancellationToken = default)
{
try
{
var requestData = new
{
ServerIds = serverIds,
Command = command,
Timeout = timeoutSeconds,
Parallel = parallel
};
var content = new StringContent(
JsonConvert.SerializeObject(requestData),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync(
"api/servers/batch-execute",
content,
cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<BatchCommandResult>(responseContent);
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new BatchCommandResult
{
Success = false,
Message = error?.Message ?? "批量执行命令失败",
Results = new Dictionary<int, CommandResult>()
};
}
}
catch (TaskCanceledException)
{
return new BatchCommandResult
{
Success = false,
Message = "批量命令执行已取消",
Results = new Dictionary<int, CommandResult>()
};
}
catch (Exception ex)
{
return new BatchCommandResult
{
Success = false,
Message = $"批量执行命令时发生错误: {ex.Message}",
Results = new Dictionary<int, CommandResult>()
};
}
}
public async Task<BatchScriptResult> ExecuteBatchScriptAsync(
IEnumerable<int> serverIds,
string scriptContent,
string scriptType = "bash",
Dictionary<string, string> parameters = null,
int timeoutSeconds = 300,
bool parallel = true,
CancellationToken cancellationToken = default)
{
try
{
var requestData = new
{
ServerIds = serverIds,
ScriptContent = scriptContent,
ScriptType = scriptType,
Parameters = parameters ?? new Dictionary<string, string>(),
Timeout = timeoutSeconds,
Parallel = parallel
};
var content = new StringContent(
JsonConvert.SerializeObject(requestData),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync(
"api/servers/batch-execute-script",
content,
cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<BatchScriptResult>(responseContent);
}
else
{
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
return new BatchScriptResult
{
Success = false,
Message = error?.Message ?? "批量执行脚本失败",
Results = new Dictionary<int, ScriptResult>()
};
}
}
catch (TaskCanceledException)
{
return new BatchScriptResult
{
Success = false,
Message = "批量脚本执行已取消",
Results = new Dictionary<int, ScriptResult>()
};
}
catch (Exception ex)
{
return new BatchScriptResult
{
Success = false,
Message = $"批量执行脚本时发生错误: {ex.Message}",
Results = new Dictionary<int, ScriptResult>()
};
}
}
}
public class CommandResult
{
public bool Success { get; set; }
public int ExitCode { get; set; }
public string Output { get; set; }
public string Error { get; set; }
}
public class ScriptResult
{
public bool Success { get; set; }
public int ExitCode { get; set; }
public string Output { get; set; }
public string Error { get; set; }
public double ExecutionTime { get; set; }
}
public class BatchCommandResult
{
public bool Success { get; set; }
public string Message { get; set; }
public Dictionary<int, CommandResult> Results { get; set; }
}
public class BatchScriptResult
{
public bool Success { get; set; }
public string Message { get; set; }
public Dictionary<int, ScriptResult> Results { get; set; }
}
}
远程执行界面
csharp
// RemoteExecutionViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using OpsManager.Client.Core;
using OpsManager.Client.Models;
using OpsManager.Client.Services;
namespace OpsManager.Client.ViewModels
{
public class RemoteExecutionViewModel : ViewModelBase, IRefreshable
{
private readonly IRemoteExecutionService _remoteExecutionService;
private readonly IServerService _serverService;
private readonly IDialogService _dialogService;
private bool _isLoading;
private string _command;
private string _scriptContent;
private string _scriptType;
private int _timeout;
private bool _isExecuting;
private CancellationTokenSource _cancellationTokenSource;
private ObservableCollection<ServerViewModel> _servers;
private ObservableCollection<ExecutionResultViewModel> _results;
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
}
}
public string Command
{
get => _command;
set
{
_command = value;
OnPropertyChanged();
ExecuteCommandCommand.RaiseCanExecuteChanged();
}
}
public string ScriptContent
{
get => _scriptContent;
set
{
_scriptContent = value;
OnPropertyChanged();
ExecuteScriptCommand.RaiseCanExecuteChanged();
}
}
public string ScriptType
{
get => _scriptType;
set
{
_scriptType = value;
OnPropertyChanged();
}
}
public int Timeout
{
get => _timeout;
set
{
_timeout = value;
OnPropertyChanged();
}
}
public bool IsExecuting
{
get => _isExecuting;
set
{
_isExecuting = value;
OnPropertyChanged();
ExecuteCommandCommand.RaiseCanExecuteChanged();
ExecuteScriptCommand.RaiseCanExecuteChanged();
CancelExecutionCommand.RaiseCanExecuteChanged();
}
}
public ObservableCollection<ServerViewModel> Servers
{
get => _servers;
set
{
_servers = value;
OnPropertyChanged();
}
}
public ObservableCollection<ExecutionResultViewModel> Results
{
get => _results;
set
{
_results = value;
OnPropertyChanged();
}
}
public RelayCommand LoadServersCommand { get; }
public RelayCommand<ServerViewModel> SelectAllCommand { get; }
public RelayCommand<ServerViewModel> UnselectAllCommand { get; }
public RelayCommand ExecuteCommandCommand { get; }
public RelayCommand ExecuteScriptCommand { get; }
public RelayCommand CancelExecutionCommand { get; }
public RelayCommand ClearResultsCommand { get; }
public RemoteExecutionViewModel(
IRemoteExecutionService remoteExecutionService,
IServerService serverService,
IDialogService dialogService)
{
_remoteExecutionService = remoteExecutionService ?? throw new ArgumentNullException(nameof(remoteExecutionService));
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
Servers = new ObservableCollection<ServerViewModel>();
Results = new ObservableCollection<ExecutionResultViewModel>();
LoadServersCommand = new RelayCommand(ExecuteLoadServersAsync);
SelectAllCommand = new RelayCommand<ServerViewModel>(_ => ExecuteSelectAll());
UnselectAllCommand = new RelayCommand<ServerViewModel>(_ => ExecuteUnselectAll());
ExecuteCommandCommand = new RelayCommand(ExecuteCommandAsync, CanExecuteCommand);
ExecuteScriptCommand = new RelayCommand(ExecuteScriptAsync, CanExecuteScript);
CancelExecutionCommand = new RelayCommand(ExecuteCancelExecution, CanCancelExecution);
ClearResultsCommand = new RelayCommand(ExecuteClearResults);
// 初始化默认值
ScriptType = "bash";
Timeout = 60;
// 加载服务器列表
ExecuteLoadServersAsync();
}
public void Refresh()
{
ExecuteLoadServersAsync();
}
private async void ExecuteLoadServersAsync()
{
try
{
IsLoading = true;
// 获取服务器列表
var servers = await _serverService.GetAllServersAsync();
// 转换为视图模型
var serverViewModels = servers.Select(s => new ServerViewModel
{
ID = s.ServerID,
Hostname = s.Hostname,
IPAddress = s.IPAddress,
OSType = s.OSType,
Status = s.Status,
IsSelected = false
}).ToList();
// 更新服务器列表
Servers.Clear();
foreach (var server in serverViewModels)
{
Servers.Add(server);
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("加载失败", $"加载服务器列表失败: {ex.Message}");
Logger.Error(ex, "加载服务器列表失败");
}
finally
{
IsLoading = false;
}
}
private void ExecuteSelectAll()
{
foreach (var server in Servers)
{
server.IsSelected = true;
}
}
private void ExecuteUnselectAll()
{
foreach (var server in Servers)
{
server.IsSelected = false;
}
}
private bool CanExecuteCommand()
{
return !IsExecuting && !string.IsNullOrWhiteSpace(Command) && Servers.Any(s => s.IsSelected);
}
private async void ExecuteCommandAsync()
{
try
{
// 检查是否有选中的服务器
var selectedServers = Servers.Where(s => s.IsSelected).ToList();
if (!selectedServers.Any())
{
await _dialogService.ShowWarningAsync("警告", "请至少选择一台服务器");
return;
}
// 开始执行
IsExecuting = true;
// 创建取消令牌
_cancellationTokenSource = new CancellationTokenSource();
// 清空之前的结果
Results.Clear();
// 为每个选中的服务器创建结果项
foreach (var server in selectedServers)
{
Results.Add(new ExecutionResultViewModel
{
ServerID = server.ID,
ServerName = server.Hostname,
Status = "等待中...",
Output = "",
Error = ""
});
}
// 获取选中服务器的ID列表
var serverIds = selectedServers.Select(s => s.ID).ToList();
// 批量执行命令
var result = await _remoteExecutionService.ExecuteBatchCommandAsync(
serverIds,
Command,
Timeout,
true,
_cancellationTokenSource.Token);
// 处理结果
if (result.Success)
{
// 更新每个服务器的执行结果
foreach (var serverResult in result.Results)
{
var serverId = serverResult.Key;
var commandResult = serverResult.Value;
var resultViewModel = Results.FirstOrDefault(r => r.ServerID == serverId);
if (resultViewModel != null)
{
resultViewModel.Status = commandResult.Success ? "成功" : "失败";
resultViewModel.ExitCode = commandResult.ExitCode;
resultViewModel.Output = commandResult.Output;
resultViewModel.Error = commandResult.Error;
}
}
}
else
{
await _dialogService.ShowErrorAsync("执行失败", result.Message);
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("执行失败", $"执行命令时发生错误: {ex.Message}");
Logger.Error(ex, "执行命令失败");
}
finally
{
IsExecuting = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
}
private bool CanExecuteScript()
{
return !IsExecuting && !string.IsNullOrWhiteSpace(ScriptContent) && Servers.Any(s => s.IsSelected);
}
private async void ExecuteScriptAsync()
{
try
{
// 检查是否有选中的服务器
var selectedServers = Servers.Where(s => s.IsSelected).ToList();
if (!selectedServers.Any())
{
await _dialogService.ShowWarningAsync("警告", "请至少选择一台服务器");
return;
}
// 开始执行
IsExecuting = true;
// 创建取消令牌
_cancellationTokenSource = new CancellationTokenSource();
// 清空之前的结果
Results.Clear();
// 为每个选中的服务器创建结果项
foreach (var server in selectedServers)
{
Results.Add(new ExecutionResultViewModel
{
ServerID = server.ID,
ServerName = server.Hostname,
Status = "等待中...",
Output = "",
Error = ""
});
}
// 获取选中服务器的ID列表
var serverIds = selectedServers.Select(s => s.ID).ToList();
// 批量执行脚本
var result = await _remoteExecutionService.ExecuteBatchScriptAsync(
serverIds,
ScriptContent,
ScriptType,
null,
Timeout,
true,
_cancellationTokenSource.Token);
// 处理结果
if (result.Success)
{
// 更新每个服务器的执行结果
foreach (var serverResult in result.Results)
{
var serverId = serverResult.Key;
var scriptResult = serverResult.Value;
var resultViewModel = Results.FirstOrDefault(r => r.ServerID == serverId);
if (resultViewModel != null)
{
resultViewModel.Status = scriptResult.Success ? "成功" : "失败";
resultViewModel.ExitCode = scriptResult.ExitCode;
resultViewModel.Output = scriptResult.Output;
resultViewModel.Error = scriptResult.Error;
resultViewModel.ExecutionTime = scriptResult.ExecutionTime;
}
}
}
else
{
await _dialogService.ShowErrorAsync("执行失败", result.Message);
}
}
catch (Exception ex)
{
await _dialogService.ShowErrorAsync("执行失败", $"执行脚本时发生错误: {ex.Message}");
Logger.Error(ex, "执行脚本失败");
}
finally
{
IsExecuting = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
}
private bool CanCancelExecution()
{
return IsExecuting && _cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested;
}
private void ExecuteCancelExecution()
{
try
{
// 取消执行
_cancellationTokenSource?.Cancel();
}
catch (Exception ex)
{
Logger.Error(ex, "取消执行失败");
}
}
private void ExecuteClearResults()
{
Results.Clear();
}
}
public class ServerViewModel : ViewModelBase
{
private bool _isSelected;
public int ID { get; set; }
public string Hostname { get; set; }
public string IPAddress { get; set; }
public string OSType { get; set; }
public string Status { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public class ExecutionResultViewModel : ViewModelBase
{
public int ServerID { get; set; }
public string ServerName { get; set; }
public string Status { get; set; }
public int ExitCode { get; set; }
public string Output { get; set; }
public string Error { get; set; }
public double ExecutionTime { get; set; }
}
}
16.5.7 平台程序发布
平台程序的发布是将开发完成的软件打包并准备部署的过程。下面介绍发布桌面版C/S自动化运维平台的关键步骤。
版本管理
在发布前,需要进行适当的版本管理:
csharp
// 版本信息类
public static class VersionInfo
{
// 应用程序的版本号
public const string Version = "1.0.0";
// 内部版本号,用于区分不同的构建
public const string BuildNumber = "10001";
// 发布日期
public static readonly DateTime ReleaseDate = new DateTime(2023, 5, 28);
// 版本类型
public const string VersionType = "Release"; // "Alpha", "Beta", "RC", "Release"
// 完整版本字符串
public static string FullVersionString => $"{Version} ({VersionType}) Build {BuildNumber}";
// 版权信息
public const string Copyright = "Copyright © 2023 Your Company. All rights reserved.";
// 是否是调试版本
#if DEBUG
public const bool IsDebugBuild = true;
#else
public const bool IsDebugBuild = false;
#endif
}
打包脚本
使用PowerShell脚本自动化打包过程:
powershell
# build_and_package.ps1
param (
[string]$Configuration = "Release",
[string]$Version = "1.0.0",
[string]$BuildNumber = (Get-Date -Format "yyyyMMddHHmm")
)
# 项目路径
$solutionPath = ".\OpsManager.sln"
$clientProjectPath = ".\OpsManager.Client\OpsManager.Client.csproj"
$serverProjectPath = ".\OpsManager.Server\OpsManager.Server.csproj"
$outputPath = ".\Release"
$packagePath = "$outputPath\Packages"
# 确保输出目录存在
if (!(Test-Path $outputPath)) {
New-Item -ItemType Directory -Path $outputPath | Out-Null
}
if (!(Test-Path $packagePath)) {
New-Item -ItemType Directory -Path $packagePath | Out-Null
}
# 更新版本信息
Write-Host "Updating version information to $Version (Build $BuildNumber)..." -ForegroundColor Cyan
# 更新AssemblyInfo.cs文件
$assemblyInfoPath = ".\OpsManager.Client\Properties\AssemblyInfo.cs"
$assemblyInfo = Get-Content $assemblyInfoPath
$assemblyInfo = $assemblyInfo -replace 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyVersion(`"$Version`")"
$assemblyInfo = $assemblyInfo -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyFileVersion(`"$Version`")"
$assemblyInfo | Set-Content $assemblyInfoPath
# 更新版本信息类
$versionInfoPath = ".\OpsManager.Client\Utils\VersionInfo.cs"
$versionInfo = Get-Content $versionInfoPath
$versionInfo = $versionInfo -replace 'public const string Version = "[0-9]+(\.([0-9]+|\*)){1,3}";', "public const string Version = `"$Version`";"
$versionInfo = $versionInfo -replace 'public const string BuildNumber = "[0-9]+";', "public const string BuildNumber = `"$BuildNumber`";"
$versionInfo = $versionInfo -replace 'public static readonly DateTime ReleaseDate = new DateTime\([0-9]+, [0-9]+, [0-9]+\);', "public static readonly DateTime ReleaseDate = new DateTime($((Get-Date).Year), $((Get-Date).Month), $((Get-Date).Day));"
$versionInfo | Set-Content $versionInfoPath
# 编译解决方案
Write-Host "Building solution..." -ForegroundColor Cyan
dotnet restore $solutionPath
dotnet build $solutionPath -c $Configuration
# 发布客户端
Write-Host "Publishing client application..." -ForegroundColor Cyan
dotnet publish $clientProjectPath -c $Configuration -o "$outputPath\Client" --self-contained true -r win-x64 /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
# 发布服务器
Write-Host "Publishing server application..." -ForegroundColor Cyan
dotnet publish $serverProjectPath -c $Configuration -o "$outputPath\Server"
# 创建客户端安装包
Write-Host "Creating client installer package..." -ForegroundColor Cyan
$clientPackageName = "OpsManager-Client-$Version.zip"
Compress-Archive -Path "$outputPath\Client\*" -DestinationPath "$packagePath\$clientPackageName" -Force
# 创建服务器安装包
Write-Host "Creating server installer package..." -ForegroundColor Cyan
$serverPackageName = "OpsManager-Server-$Version.zip"
Compress-Archive -Path "$outputPath\Server\*" -DestinationPath "$packagePath\$serverPackageName" -Force
# 创建代理安装包
Write-Host "Creating agent installer packages..." -ForegroundColor Cyan
$winAgentPackageName = "OpsManager-Agent-Windows-$Version.zip"
$linuxAgentPackageName = "OpsManager-Agent-Linux-$Version.tar.gz"
Compress-Archive -Path ".\OpsManager.Agent\bin\$Configuration\net6.0\win-x64\publish\*" -DestinationPath "$packagePath\$winAgentPackageName" -Force
# 使用tar命令创建Linux代理包
Set-Location ".\OpsManager.Agent\bin\$Configuration\net6.0\linux-x64\publish"
tar -czf "$packagePath\$linuxAgentPackageName" *
Set-Location -Path (Get-Location).Path.Substring(0, (Get-Location).Path.IndexOf("\OpsManager.Agent"))
# 创建发布说明
Write-Host "Creating release notes..." -ForegroundColor Cyan
$releaseNotesPath = "$packagePath\ReleaseNotes-$Version.txt"
$releaseDate = Get-Date -Format "yyyy-MM-dd"
@"
# OpsManager $Version Release Notes
Release Date: $releaseDate
Build Number: $BuildNumber
## New Features
- Initial release of OpsManager platform
- Server asset management
- Monitoring and alerting
- Remote control
- Task automation
- Software management
- Reporting
## Improvements
- N/A
## Bug Fixes
- N/A
## Known Issues
- N/A
## Installation Instructions
1. For client: Extract OpsManager-Client-$Version.zip and run setup.exe
2. For server: See installation guide in documentation
## System Requirements
- Client: Windows 10/11 with .NET Framework 4.8+
- Server: Windows Server 2019/2022 or Linux with .NET Core 6.0+
"@ | Set-Content $releaseNotesPath
Write-Host "Build and packaging completed successfully!" -ForegroundColor Green
Write-Host "Client package: $packagePath\$clientPackageName" -ForegroundColor Yellow
Write-Host "Server package: $packagePath\$serverPackageName" -ForegroundColor Yellow
Write-Host "Windows Agent package: $packagePath\$winAgentPackageName" -ForegroundColor Yellow
Write-Host "Linux Agent package: $packagePath\$linuxAgentPackageName" -ForegroundColor Yellow
Write-Host "Release Notes: $releaseNotesPath" -ForegroundColor Yellow
安装程序制作
使用WiX Toolset创建Windows安装程序:
xml
<!-- Product.wxs -->
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*"
Name="OpsManager Client"
Language="1033"
Version="1.0.0.0"
Manufacturer="Your Company"
UpgradeCode="PUT-GUID-HERE">
<Package InstallerVersion="200"
Compressed="yes"
InstallScope="perMachine"
Description="OpsManager Client Installer"
Comments="Installs the OpsManager client application" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate EmbedCab="yes" />
<Feature Id="ProductFeature" Title="OpsManager Client" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentRef Id="ApplicationShortcut" />
</Feature>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<UIRef Id="WixUI_InstallDir" />
<WixVariable Id="WixUILicenseRtf" Value="License.rtf" />
<WixVariable Id="WixUIBannerBmp" Value="banner.bmp" />
<WixVariable Id="WixUIDialogBmp" Value="dialog.bmp" />
<Property Id="SERVER_URL" Value="https://localhost:5001" />
<Property Id="INSTALL_DIR" Value="[ProgramFilesFolder]OpsManager" />
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="OpsManager">
<!-- Application files will go here -->
</Directory>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="OpsManager"/>
</Directory>
<Directory Id="DesktopFolder" Name="Desktop" />
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- Add application files here -->
<Component Id="MainExecutable" Guid="*">
<File Id="OpsManagerEXE" Source="$(var.BinDir)\OpsManager.Client.exe" KeyPath="yes" />
</Component>
<!-- Add other application files here -->
</ComponentGroup>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="OpsManager"
Description="OpsManager Client"
Target="[INSTALLFOLDER]OpsManager.Client.exe"
WorkingDirectory="INSTALLFOLDER"/>
<Shortcut Id="DesktopShortcut"
Directory="DesktopFolder"
Name="OpsManager"
Description="OpsManager Client"
Target="[INSTALLFOLDER]OpsManager.Client.exe"
WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\OpsManager" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
</Fragment>
</Wix>
自动更新配置
配置自动更新服务:
xml
<!-- update.config -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="UpdateCheckUrl" value="https://example.com/updates/check" />
<add key="UpdateDownloadUrl" value="https://example.com/updates/download" />
<add key="UpdateCheckInterval" value="240" /> <!-- In minutes -->
<add key="AutomaticUpdates" value="true" />
<add key="UpdateLogPath" value="%APPDATA%\OpsManager\Logs\Updates" />
</appSettings>
</configuration>
通过以上步骤,我们完成了桌面版C/S自动化运维平台的设计和实现。该平台提供了丰富的功能,包括服务器资产管理、监控告警、远程控制、自动化任务、软件管理等,能够满足中小型企业IT部门的自动化运维需求。采用C/S架构,该平台具有响应速度快、用户体验好、功能丰富等特点,是企业IT基础设施管理的有力工具。
更多推荐


所有评论(0)